def reserve_system_coin(geb: GfDeployment, c: Collateral, usr: Address, amount: Wad, extra_collateral=Wad.from_number(1)): assert isinstance(geb, GfDeployment) assert isinstance(c, Collateral) assert isinstance(usr, Address) assert isinstance(amount, Wad) assert amount > Wad(0) # Determine how much collateral is needed collateral_type = geb.safe_engine.collateral_type(c.collateral_type.name) accumulated_rate = collateral_type.accumulated_rate # Ray safety_price = collateral_type.safety_price # Ray assert accumulated_rate >= Ray.from_number(1) collateral_required = Wad((Ray(amount) / safety_price) * accumulated_rate) * extra_collateral + Wad(1) print(f'accumulated_rate {accumulated_rate}') print(f'extra_collateral {extra_collateral}') print(f'current safety price {safety_price}') print( f'collateral_required for {str(amount)} system_coin is {str(collateral_required)}' ) wrap_eth(geb, usr, collateral_required) c.approve(usr) assert c.adapter.join(usr, collateral_required).transact(from_address=usr) assert geb.safe_engine.modify_safe_collateralization( c.collateral_type, usr, collateral_required, amount).transact(from_address=usr) assert geb.safe_engine.safe(c.collateral_type, usr).generated_debt >= amount
def test_process_safe(self, geb, our_address): collateral_type = geb.collaterals['ETH-A'].collateral_type safe = geb.safe_engine.safe(collateral_type, our_address) assert safe.generated_debt > Wad(0) assert geb.safe_engine.collateral_type( collateral_type.name).accumulated_rate > Ray(0) assert geb.global_settlement.final_coin_per_collateral_price( collateral_type) > Ray(0) owe = Ray(safe.generated_debt) * geb.safe_engine.collateral_type( collateral_type.name ).accumulated_rate * geb.global_settlement.final_coin_per_collateral_price( collateral_type) assert owe > Ray(0) wad = min(Ray(safe.locked_collateral), owe) print(f"owe={owe} wad={wad}") assert geb.global_settlement.process_safe(collateral_type, our_address).transact() assert geb.safe_engine.safe(collateral_type, our_address).generated_debt == Wad(0) assert geb.safe_engine.safe(collateral_type, our_address).locked_collateral > Wad(0) assert geb.safe_engine.debt_balance( geb.accounting_engine.address) > Rad(0) assert geb.safe_engine.global_debt() > Rad(0) assert geb.safe_engine.global_unbacked_debt() > Rad(0)
def is_safe_safe(collateral_type: CollateralType, safe: SAFE) -> bool: assert isinstance(safe, SAFE) assert safe.generated_debt is not None assert collateral_type.accumulated_rate is not None assert safe.locked_collateral is not None assert collateral_type.safety_price is not None #print(f'art={safe.generated_debt} * rate={collateral_type.rate} <=? locked_collateral={safe.locked_collateral} * spot={collateral_type.spot}') return (Ray(safe.generated_debt) * collateral_type.accumulated_rate ) <= Ray(safe.locked_collateral) * collateral_type.safety_price
def cleanup_safe(geb: GfDeployment, collateral: Collateral, address: Address): assert isinstance(geb, GfDeployment) assert isinstance(collateral, Collateral) assert isinstance(address, Address) safe = geb.safe_engine.safe(collateral.collateral_type, address) collateral_type = geb.safe_engine.collateral_type( collateral.collateral_type.name) # If tax_collector.tax_single has been called, we won't have sufficient system_coin to repay the SAFE #if collateral_type.accumulated_rate > Ray.from_number(1): # return # Return if this address doens't have enough system to coin to repay full debt amount_to_raise = Wad( Ray(safe.generated_debt) * collateral_type.accumulated_rate) if amount_to_raise > geb.system_coin.balance_of(address): return # Repay borrowed system coin geb.approve_system_coin(address) # Put all the user's system coin back into the safe engine if geb.system_coin.balance_of(address) >= Wad(0): assert geb.system_coin_adapter.join( address, geb.system_coin.balance_of(address)).transact(from_address=address) amount_to_raise = Wad( Ray(safe.generated_debt) * collateral_type.accumulated_rate) print( f'amount_to_raise={str(amount_to_raise)}, rate={str(collateral_type.accumulated_rate)}, system_coin={str(geb.safe_engine.coin_balance(address))}' ) if safe.generated_debt > Wad(0): wrap_modify_safe_collateralization(geb, collateral, address, Wad(0), amount_to_raise * -1) # Withdraw collateral collateral.approve(address) safe = geb.safe_engine.safe(collateral.collateral_type, address) # delta_collateral = Wad((Ray(safe.generated_debt) * collateral_type.accumulated_rate) / collateral_type.safety_price) # print(f'delta_collateral={str(delta_collateral)}, locked_collateral={str(safe.locked_collateral)}') if safe.generated_debt == Wad(0) and safe.locked_collateral > Wad(0): wrap_modify_safe_collateralization(geb, collateral, address, safe.locked_collateral * -1, Wad(0)) assert collateral.adapter.exit( address, geb.safe_engine.token_collateral( collateral.collateral_type, address)).transact(from_address=address) TestSAFEEngine.ensure_clean_safe(geb, collateral, address)
def max_delta_debt(geb: GfDeployment, collateral: Collateral, our_address: Address) -> Wad: assert isinstance(geb, GfDeployment) assert isinstance(collateral, Collateral) assert isinstance(our_address, Address) print("max_delta_debt") print( f"Liquidation price {geb.safe_engine.collateral_type(collateral.collateral_type.name).liquidation_price}" ) print( f"Safety price {geb.safe_engine.collateral_type(collateral.collateral_type.name).safety_price}" ) safe = geb.safe_engine.safe(collateral.collateral_type, our_address) collateral_type = geb.safe_engine.collateral_type( collateral.collateral_type.name) # change in debt = (collateral balance * collateral price with safety margin) - SAFE's stablecoin debt delta_debt = safe.locked_collateral * collateral_type.safety_price - Wad( Ray(safe.generated_debt) * collateral_type.accumulated_rate) print(f"max_delta_debt for collateral_type {collateral_type.name}") print( f"delta debt: {delta_debt} = locked_collateral {safe.locked_collateral} * safety_price {collateral_type.safety_price} - debt {Ray(safe.generated_debt)}" ) # change in debt must also take the rate into account delta_debt = Wad(Ray(delta_debt) / collateral_type.accumulated_rate) # prevent the change in debt from exceeding the collateral debt ceiling if (Rad(safe.generated_debt) + Rad(delta_debt)) >= collateral_type.debt_ceiling: print("max_delta_debt is avoiding collateral debt ceiling") delta_debt = Wad(collateral_type.debt_ceiling - Rad(safe.generated_debt)) # prevent the change in debt from exceeding the total debt ceiling debt = geb.safe_engine.global_debt() + Rad( collateral_type.accumulated_rate * delta_debt) debt_ceiling = Rad(geb.safe_engine.global_debt_ceiling()) if (debt + Rad(delta_debt)) >= debt_ceiling: print( f"debt {debt} + delta_debt {delta_debt} >= {debt_ceiling}; max_delta_debt is avoiding total debt ceiling" ) delta_debt = Wad(debt - Rad(safe.generated_debt)) # ensure we've met the debt_floor cutoff if Rad(safe.generated_debt + delta_debt) < collateral_type.debt_floor: print( f"max_delta_debt is being bumped from {safe.generated_debt + delta_debt} to {collateral_type.debt_floor} to reach debt_floor cutoff" ) delta_debt = Wad(collateral_type.debt_floor) return delta_debt
def test_collateral_type(self, geb): assert geb.safe_engine.collateral_type('XXX') == CollateralType( 'XXX', accumulated_rate=Ray(0), safe_collateral=Wad(0), safe_debt=Wad(0), safety_price=Ray(0), debt_ceiling=Rad(0), debt_floor=Rad(0)) collateral_type = geb.collaterals["ETH-C"].collateral_type representation = repr(collateral_type) assert "ETH-C" in representation
def get_collateral_median_price(self) -> Ray: """Returns the market price from system coin oracle. Returns: System coin market price """ return Ray(self._contract.functions.getCollateralMedianPrice().call())
def final_coin_per_collateral_price( self, collateral_type: CollateralType) -> Ray: """Shutdown price for the collateral""" assert isinstance(collateral_type, CollateralType) return Ray( self._contract.functions.finalCoinPerCollateralPrice( collateral_type.toBytes()).call())
def test_get_underwater_safes(self, geb: GfDeployment, keeper: SettlementKeeper, guy_address: Address, our_address: Address): print_out("test_get_underwater_safes") collateral_types = keeper.get_collateral_types() prev_underwater_safes = len( keeper.get_underwater_safes(collateral_types)) previous_eth_price = open_underwater_safe(geb, geb.collaterals['ETH-A'], guy_address) open_safe(geb, geb.collaterals['ETH-C'], our_address) safes = keeper.get_underwater_safes(collateral_types) assert type(safes) is list assert all(isinstance(x, SAFE) for x in safes) assert len(safes) == prev_underwater_safes + 1 assert safes[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_debt_auction fail set_collateral_price(geb, geb.collaterals['ETH-A'], Wad(previous_eth_price * Ray.from_number(1.0001))) pytest.global_safes = safes
def per_second_discount_update_rate(self) -> Ray: """Returns the perSecondDiscountUpdateRate Returns: The per second discount update rate """ return Ray( self._contract.functions.perSecondDiscountUpdateRate().call())
def is_safe_critical(collateral_type: CollateralType, safe: SAFE) -> bool: assert isinstance(safe, SAFE) assert safe.generated_debt is not None assert collateral_type.accumulated_rate is not None assert safe.locked_collateral is not None assert collateral_type.safety_price is not None #print(f'art={safe.generated_debt} * rate={collateral_type.rate} <=? locked_collateral={safe.locked_collateral} * spot={collateral_type.spot}') print("in is_safe_critical()") print(f"debt: {Ray(safe.generated_debt)}") print(f"rate: {collateral_type.accumulated_rate}") print(f"locked collateral: {Ray(safe.locked_collateral)}") print(f"liq price: {collateral_type.liquidation_price}") print(f"safety price: {collateral_type.safety_price}") return True return (Ray(safe.generated_debt) * collateral_type.accumulated_rate) > Ray( safe.locked_collateral) * collateral_type.liquidation_price
def bid(self, id: int, price: Wad ) -> Tuple[Optional[Wad], Optional[Transact], Optional[Rad]]: assert isinstance(id, int) assert isinstance(price, Wad) bid = self.debt_auction_house.bids(id) redemption_price = self.geb.oracle_relayer.redemption_price() our_amount = bid.bid_amount * redemption_price / Rad(price) if Ray(our_amount) * self.bid_decrease <= Ray( bid.amount_to_sell) and our_amount < Rad(bid.amount_to_sell): return price, self.debt_auction_house.decrease_sold_amount( id, Wad(our_amount), bid.bid_amount), bid.bid_amount else: self.logger.debug( f"our_amount {our_amount} at price {price} would not exceed the bid decrease {self.bid_decrease} for amount to sell {bid.amount_to_sell} for auction {id} and redemption price {redemption_price}" ) return None, None, None
def test_freeze_collateral_type(self, geb): collateral_type = geb.collaterals['ETH-A'].collateral_type assert geb.global_settlement.freeze_collateral_type( collateral_type).transact() assert geb.global_settlement.collateral_total_debt( collateral_type) > Wad(0) assert geb.global_settlement.final_coin_per_collateral_price( collateral_type) > Ray(0)
def test_getters(self, geb): assert not geb.global_settlement.contract_enabled() assert datetime.utcnow() - timedelta( minutes=5) < geb.global_settlement.shutdown_time( ) < datetime.utcnow() assert geb.global_settlement.shutdown_cooldown() >= 0 assert geb.global_settlement.outstanding_coin_supply() >= Rad(0) for collateral in geb.collaterals.values(): collateral_type = collateral.collateral_type assert geb.global_settlement.final_coin_per_collateral_price( collateral_type) == Ray(0) assert geb.global_settlement.collateral_shortfall( collateral_type) == Wad(0) assert geb.global_settlement.collateral_total_debt( collateral_type) == Wad(0) assert geb.global_settlement.collateral_cash_price( collateral_type) == Ray(0)
def get_underwater_safes(self, collateral_types: List) -> List[SAFE]: """ With all safes every frobbed, compile and return a list safes that are under-collateralized up to 100% """ underwater_safes = [] self.logger.info(f'Getting underwater safes for {collateral_types}') for collateral_type in collateral_types: safe_history = SAFEHistory(self.web3, self.geb, collateral_type, self.deployment_block, self.arguments.graph_endpoint) safes = safe_history.get_safes() self.logger.info( f'Collected {len(safes)} safes from {collateral_type}') for i, safe in enumerate(safes.values()): safe.collateral_type = self.geb.safe_engine.collateral_type( safe.collateral_type.name) safety_ratio = self.geb.oracle_relayer.safety_c_ratio( safe.collateral_type) debt = Ray(safe.generated_debt ) * safe.collateral_type.accumulated_rate collateral = Ray( safe.locked_collateral ) * safe.collateral_type.safety_price * safety_ratio # Check if underwater -> # safe.generated_debt * collateral_type.accumulated_rate > # safe.locked_collateral * collateral_type.safety_price * oracle_relayer.safety_c_ratio[collateral_type] if debt > collateral: underwater_safes.append(safe) if i % 100 == 0: self.logger.info( f'Processed {i} safes of {collateral_type.name}') self.logger.info( f'Found {len(underwater_safes)} underwater safes for all collateral-types' ) return underwater_safes
def wipe_debt(geb: GfDeployment, collateral: Collateral, address: Address): safe = geb.safe_engine.safe(collateral.collateral_type, address) assert Rad(safe.generated_debt) >= geb.safe_engine.coin_balance(address) delta_collateral = Ray(geb.safe_engine.coin_balance( address)) / geb.safe_engine.collateral_type( collateral.collateral_type.name).accumulated_rate wrap_modify_safe_collateralization( geb, collateral, address, Wad(0), Wad(delta_collateral) * -1) #because there is residual state on the testchain assert geb.safe_engine.coin_balance(address) <= Rad( Wad(1)) # pesky dust amount in Dai amount
def test_multiply_by_ray(self): assert Wad.from_number(2) * Ray.from_number(3) == Wad.from_number(6) assert Wad.from_number(2) * Ray(3) == Wad(0) assert Wad(2) * Ray(499999999999999999999999999) == Wad(0) assert Wad(2) * Ray(500000000000000000000000000) == Wad(1) assert Wad(2) * Ray(999999999999999999999999999) == Wad(1) assert Wad(2) * Ray(1000000000000000000000000000) == Wad(2)
def test_should_cast_to_int(self): assert int(Ray.from_number(-4.5)) == -4 assert int(Ray.from_number(0.99)) == 0 assert int(Ray.from_number(1)) == 1 assert int(Ray.from_number(1.0)) == 1 assert int(Ray.from_number(1.5)) == 1 assert int(Ray.from_number(1.9999999999)) == 1
def test_should_cast_to_float(self): assert float(Ray.from_number(-4.5)) == -4.5 assert float(Ray.from_number(0.99)) == 0.99 assert float(Ray.from_number(1)) == 1.0 assert float(Ray.from_number(1.0)) == 1.0 assert float(Ray.from_number(1.5)) == 1.5 assert float(Ray.from_number(1.9999999999)) == 1.9999999999
def test_settlement_keeper(self, geb: GfDeployment, keeper: SettlementKeeper, our_address: Address, other_address: Address): print_out("test_settlement_keeper") collateral_types = keeper.get_collateral_types() safes = pytest.global_safes auctions = pytest.global_auctions for collateral_type in collateral_types: # Check if freeze_collateral_type(collateral_type) called on all collateral_types assert geb.global_settlement.final_coin_per_collateral_price( collateral_type) > Ray(0) # Check if calculate_cash_price(collateral_type) called on all collateral_types assert geb.global_settlement.collateral_cash_price( collateral_type) > Ray(0) # All underwater safes present before ES have been processed for i in safes: safe = geb.safe_engine.safe(i.collateral_type, i.address) assert safe.generated_debt == Wad(0) # All auctions active before settlement have been terminated prematurely for collateral_type in auctions["collateral_auctions"].keys(): for auction in auctions["collateral_auctions"][collateral_type]: assert geb.collaterals[ collateral_type].collateral_auction_house.bids( auction.id).amount_to_sell == Wad(0) for auction in auctions["surplus_auctions"]: assert geb.surplus_auction_house.bids( auction.id).amount_to_sell == Rad(0) for auction in auctions["debt_auctions"]: assert geb.debt_auction_house.bids( auction.id).amount_to_sell == Wad(0) # setOutstandingCoinSupply() has been called assert geb.global_settlement.outstanding_coin_supply() != Rad(0)
def max_delta_debt(geb: GfDeployment, collateral: Collateral, our_address: Address) -> Wad: """Determines how much stablecoin should be reserved in an `safe` to make it as poorly collateralized as possible, such that a small change to the collateral price could trip the liquidation ratio.""" assert isinstance(geb, GfDeployment) assert isinstance(collateral, Collateral) assert isinstance(our_address, Address) safe = geb.safe_engine.safe(collateral.collateral_type, our_address) collateral_type = geb.safe_engine.collateral_type( collateral.collateral_type.name) # change in generated debt = (collateral balance * collateral price with safety margin) - SAFE's stablecoin debt delta_debt = safe.locked_collateral * collateral_type.safety_price - Wad( Ray(safe.generated_debt) * collateral_type.accumulated_rate) # change in debt must also take the rate into account delta_debt = delta_debt * Wad( Ray.from_number(1) / collateral_type.accumulated_rate) # prevent the change in debt from exceeding the collateral debt ceiling if (Rad(safe.generated_debt) + Rad(delta_debt)) >= collateral_type.debt_ceiling: print("max_delta_debt is avoiding collateral debt ceiling") delta_debt = Wad(collateral_type.debt_ceiling - Rad(safe.generated_debt)) # prevent the change in debt from exceeding the total debt ceiling debt = geb.safe_engine.global_debt() + Rad( collateral_type.accumulated_rate * delta_debt) debt_ceiling = Rad(collateral_type.debt_ceiling) if (debt + Rad(delta_debt)) >= debt_ceiling: print("max_delta_debt is avoiding total debt ceiling") delta_debt = Wad(debt - Rad(safe.generated_debt)) assert delta_debt > Wad(0) return delta_debt
def test_should_reject_comparison_with_ints(self): with pytest.raises(ArithmeticError): assert Ray(1000) == 100 with pytest.raises(ArithmeticError): assert Ray(1000) != 999 with pytest.raises(ArithmeticError): assert Ray(1000) > 999 with pytest.raises(ArithmeticError): assert Ray(999) < 1000 with pytest.raises(ArithmeticError): assert Ray(999) <= 1000 with pytest.raises(ArithmeticError): assert Ray(1000) <= 1000 with pytest.raises(ArithmeticError): assert Ray(1000) >= 1000 with pytest.raises(ArithmeticError): assert Ray(1000) >= 999
def test_should_reject_comparison_with_rays(self): with pytest.raises(ArithmeticError): assert Rad(1000) == Ray(1000) with pytest.raises(ArithmeticError): assert Rad(1000) != Ray(999) with pytest.raises(ArithmeticError): assert Rad(1000) > Ray(999) with pytest.raises(ArithmeticError): assert Rad(999) < Ray(1000) with pytest.raises(ArithmeticError): assert Rad(999) <= Ray(1000) with pytest.raises(ArithmeticError): assert Rad(1000) <= Ray(1000) with pytest.raises(ArithmeticError): assert Rad(1000) >= Ray(1000) with pytest.raises(ArithmeticError): assert Rad(1000) >= Ray(999)
def test_should_reject_comparison_with_wads(self): with pytest.raises(ArithmeticError): assert Ray(1000) == Wad(1000) with pytest.raises(ArithmeticError): assert Ray(1000) != Wad(999) with pytest.raises(ArithmeticError): assert Ray(1000) > Wad(999) with pytest.raises(ArithmeticError): assert Ray(999) < Wad(1000) with pytest.raises(ArithmeticError): assert Ray(999) <= Wad(1000) with pytest.raises(ArithmeticError): assert Ray(1000) <= Wad(1000) with pytest.raises(ArithmeticError): assert Ray(1000) >= Wad(1000) with pytest.raises(ArithmeticError): assert Ray(1000) >= Wad(999)
def test_should_format_to_string_nicely(self): assert str(Ray(1)) == "0.000000000000000000000000001" assert str(Ray( 500000000000000000000000000)) == "0.500000000000000000000000000" assert str(Ray( 1500000000000000000000000000)) == "1.500000000000000000000000000" assert str(Ray( -1500000000000000000000000000)) == "-1.500000000000000000000000000" assert str(Ray( -500000000000000000000000000)) == "-0.500000000000000000000000000" assert str(Ray(-1)) == "-0.000000000000000000000000001"
def handle_returned_collateral(): # Handle collateral returned to the safe after a liquidation is settled available_to_generate = ( safe.locked_collateral * collateral_type.safety_price) - Wad( Ray(safe.generated_debt) * collateral_type.accumulated_rate) if available_to_generate > token.min_amount + flub_amount: logging.info( f"Attempting to generate {available_to_generate} system coin") geb.safe_engine.modify_safe_collateralization( collateral_type, our_address, Wad(0), available_to_generate).transact() system_coin_balance = Wad( geb.safe_engine.system_coin(our_address)) - Wad(1) if system_coin_balance > token.min_amount: logging.info(f"Attempting to exit {system_coin_balance} system_coin") geb.system_coin_adapter.exit(our_address, system_coin_balance).transact()
def test_exact_safety_c_ratio(self, geb): collateral_type = geb.collaterals['ETH-A'].collateral_type #set_collateral_price(geb, coll, Wad.from_number(250)) collateral_price = Wad(geb.collaterals['ETH-A'].osm.read()) geb.oracle_relayer.update_collateral_price(collateral_type) safe_collateral_type = geb.safe_engine.collateral_type('ETH-A') redemption_price = geb.oracle_relayer.redemption_price() safe_c_ratio = geb.oracle_relayer.safety_c_ratio(collateral_type) liquidation_c_ratio = geb.oracle_relayer.liquidation_c_ratio( collateral_type) calc_ratio = Ray( collateral_price ) / redemption_price / safe_collateral_type.safety_price assert safe_c_ratio == calc_ratio
def test_close_safe(self, web3, geb, our_address): collateral = geb.collaterals['ETH-A'] collateral_type = collateral.collateral_type assert geb.global_settlement.free_collateral( collateral_type).transact() assert geb.safe_engine.safe(collateral_type, our_address).locked_collateral == Wad(0) assert geb.safe_engine.token_collateral(collateral_type, our_address) > Wad(0) assert collateral.adapter.exit( our_address, geb.safe_engine.token_collateral(collateral_type, our_address)).transact() assert geb.global_settlement.shutdown_cooldown() == 0 time_travel_by(web3, 5) assert geb.global_settlement.set_outstanding_coin_supply().transact() assert geb.global_settlement.calculate_cash_price( collateral_type).transact() assert geb.global_settlement.collateral_cash_price( collateral_type) > Ray(0)
def bids(self, id: int) -> Bid: """Returns the auction details. Args: id: Auction identifier. Returns: The auction details. """ assert (isinstance(id, int)) array = self._contract.functions.bids(id).call() return IncreasingDiscountCollateralAuctionHouse.Bid( id=id, amount_to_sell=Wad(array[0]), amount_to_raise=Rad(array[1]), current_discount=Wad(array[2]), max_discount=Wad(array[3]), per_second_discount_update_rate=Ray(array[4]), latest_discount_update_time=int(array[5]), discount_increase_deadline=int(array[6]), forgone_collateral_receiver=Address(array[7]), auction_income_recipient=Address(array[8]))
def collateral_cash_price(self, collateral_type: CollateralType) -> Ray: """Final cash price for the collateral""" assert isinstance(collateral_type, CollateralType) return Ray( self._contract.functions.collateralCashPrice( collateral_type.toBytes()).call())