def liquidate_urn(cls, web3, mcd, c, gal_address, our_address): # Ensure the CDP isn't safe urn = mcd.vat.urn(c.ilk, gal_address) dart = max_dart(mcd, c, gal_address) - Wad.from_number(1) assert mcd.vat.frob(c.ilk, gal_address, Wad(0), dart).transact(from_address=gal_address) set_collateral_price(mcd, c, Wad.from_number(66)) assert not is_cdp_safe(mcd.vat.ilk(c.ilk.name), urn) # Bite and kick off the auction kick = bite(mcd, c, urn) assert kick > 0 # Bid on and win the auction auction = c.flipper.bids(kick) bid = Wad(auction.tab) + Wad(1) reserve_dai(mcd, c, our_address, bid) c.flipper.approve( mcd.vat.address, approval_function=hope_directly(from_address=our_address)) assert c.flipper.tend(kick, auction.lot, auction.tab).transact(from_address=our_address) time_travel_by(web3, c.flipper.ttl() + 1) assert c.flipper.deal(kick).transact() set_collateral_price(mcd, c, Wad.from_number(200)) urn = mcd.vat.urn(c.ilk, gal_address) assert urn.ink == Wad(0) assert urn.art == Wad(0)
def kick(web3: Web3, mcd: DssDeployment, gal_address, other_address) -> int: joy = mcd.vat.dai(mcd.vow.address) woe = (mcd.vat.sin(mcd.vow.address) - mcd.vow.sin()) - mcd.vow.ash() print(f'joy={str(joy)[:6]}, woe={str(woe)[:6]}') if woe < joy: # Bite gal CDP c = mcd.collaterals['ETH-B'] unsafe_cdp = create_unsafe_cdp(mcd, c, Wad.from_number(2), other_address, draw_dai=False) flip_kick = bite(mcd, c, unsafe_cdp) # Generate some Dai, bid on and win the flip auction without covering all the debt reserve_dai(mcd, c, gal_address, Wad.from_number(100), extra_collateral=Wad.from_number(1.1)) c.flipper.approve(mcd.vat.address, approval_function=hope_directly(from_address=gal_address)) current_bid = c.flipper.bids(flip_kick) bid = Rad.from_number(1.9) assert mcd.vat.dai(gal_address) > bid assert c.flipper.tend(flip_kick, current_bid.lot, bid).transact(from_address=gal_address) time_travel_by(web3, c.flipper.ttl()+1) assert c.flipper.deal(flip_kick).transact() flog_and_heal(web3, mcd, past_blocks=1200, kiss=False) # Kick off the flop auction woe = (mcd.vat.sin(mcd.vow.address) - mcd.vow.sin()) - mcd.vow.ash() assert mcd.vow.sump() <= woe assert mcd.vat.dai(mcd.vow.address) == Rad(0) assert mcd.vow.flop().transact(from_address=gal_address) return mcd.flopper.kicks()
def test_should_sequentially_tend_and_dent_if_price_takes_us_to_the_dent_phrase(self, kick, keeper_address): # given flipper = self.collateral.flipper (model, model_factory) = models(self.keeper, kick) # when our_bid_price = Wad.from_number(150) assert our_bid_price * flipper.bids(kick).lot > Wad(flipper.bids(1).tab) self.simulate_model_bid(self.mcd, self.collateral, model, our_bid_price) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then auction = flipper.bids(kick) assert auction.bid == auction.tab assert auction.lot == tend_lot # when reserve_dai(self.mcd, self.collateral, keeper_address, Wad(auction.tab)) self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then auction = flipper.bids(kick) assert auction.bid == auction.tab assert auction.lot < tend_lot assert round(auction.bid / Rad(auction.lot), 2) == round(Rad(our_bid_price), 2) # cleanup time_travel_by(self.web3, flipper.ttl() + 1) assert flipper.deal(kick).transact()
def test_should_take_partial_if_insufficient_dai_available(self, kick): # given (model, model_factory) = models(self.keeper, kick) (needs_redo, price, initial_lot, initial_tab) = self.clipper.status(kick) assert initial_lot == Wad.from_number(1) # and we exit all Dai out of the Vat assert self.mcd.dai_adapter.exit(self.keeper_address, Wad(self.mcd.vat.dai(self.keeper_address)))\ .transact(from_address=self.keeper_address) # when we have less Dai than we need to cover the auction our_price = Ray.from_number(187) assert our_price < price dai_needed = initial_lot * Wad(our_price) half_dai = dai_needed / Wad.from_number(2) initial_dai_balance = Wad(self.mcd.vat.dai(self.keeper_address)) if initial_dai_balance < half_dai: print(f"Reserving {half_dai - initial_dai_balance} Dai to get balance of {half_dai}") reserve_dai(self.mcd, self.dai_collateral, self.keeper_address, half_dai - initial_dai_balance) else: print(f"Abandoning {initial_dai_balance - half_dai} Dai to get balance of {half_dai}") self.mcd.vat.move(self.keeper_address, self.gal_address, Rad(initial_dai_balance - half_dai))\ .transact(from_address=keeper_address) dai_balance_before_take = Wad(self.mcd.vat.dai(self.keeper_address)) assert Wad(0) < dai_balance_before_take < dai_needed # then ensure we don't bid when the price is too high self.simulate_model_bid(model, our_price, reserve_dai_for_bid=False) self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() (needs_redo, price, lot, tab) = self.clipper.status(kick) assert lot == initial_lot assert tab == initial_tab # when we wait for the price to become appropriate while lot > Wad(0): time_travel_by(self.web3, 1) (needs_redo, auction_price, lot, tab) = self.clipper.status(kick) if auction_price < our_price: break # then ensure our bid is submitted using available Dai # self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() (needs_redo, price, lot, tab) = self.clipper.status(kick) assert Wad(0) < lot < initial_lot our_take = self.last_log() assert isinstance(our_take, Clipper.TakeLog) assert Wad(self.mcd.vat.dai(self.keeper_address)) < dai_balance_before_take # and ensure we don't place a subsequent dusty bid afterward self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() our_take2 = self.last_log() assert our_take.tx_hash == our_take2.tx_hash # cleanup self.take_below_price(kick, price, self.keeper_address)
def setup_method(self): self.web3 = web3() self.our_address = our_address(self.web3) self.keeper_address = keeper_address(self.web3) self.other_address = other_address(self.web3) self.gal_address = gal_address(self.web3) self.mcd = mcd(self.web3) self.flopper = self.mcd.flopper self.flopper.approve( self.mcd.vat.address, approval_function=hope_directly(from_address=self.keeper_address)) self.flopper.approve( self.mcd.vat.address, approval_function=hope_directly(from_address=self.other_address)) self.keeper = AuctionKeeper(args=args( f"--eth-from {self.keeper_address} " f"--type flop " f"--from-block 1 " f"--model ./bogus-model.sh"), web3=self.web3) self.keeper.approve() assert isinstance(self.keeper.gas_price, DynamicGasPrice) self.default_gas_price = self.keeper.gas_price.get_gas_price(0) reserve_dai(self.mcd, self.mcd.collaterals['ETH-C'], self.keeper_address, Wad.from_number(200.00000)) reserve_dai(self.mcd, self.mcd.collaterals['ETH-C'], self.other_address, Wad.from_number(200.00000)) self.sump = self.mcd.vow.sump() # Rad
def test_should_detect_flop(self, web3, c, mcd, other_address, keeper_address): # given a count of flop auctions reserve_dai(mcd, c, keeper_address, Wad.from_number(230)) kicks = mcd.flopper.kicks() # and an undercollateralized CDP is bitten unsafe_cdp = create_unsafe_cdp(mcd, c, Wad.from_number(1), other_address, draw_dai=False) assert mcd.cat.bite(unsafe_cdp.ilk, unsafe_cdp).transact() # when the auction ends without debt being covered time_travel_by(web3, c.flipper.tau() + 1) # then ensure testchain is in the appropriate state joy = mcd.vat.dai(mcd.vow.address) awe = mcd.vat.sin(mcd.vow.address) woe = (mcd.vat.sin(mcd.vow.address) - mcd.vow.sin()) - mcd.vow.ash() sin = mcd.vow.sin() sump = mcd.vow.sump() wait = mcd.vow.wait() assert joy < awe assert woe + sin >= sump assert wait == 0 # when self.keeper.check_flop() wait_for_other_threads() # then ensure another flop auction was kicked off assert mcd.flopper.kicks() == kicks + 1 # clean up by letting the auction expire time_travel_by(web3, mcd.flopper.tau() + 1)
def test_should_increase_gas_price_of_pending_transactions_if_model_increases_gas_price( self, mcd, c, kick, keeper): # given (model, model_factory) = models(keeper, kick) flipper = c.flipper # when bid_price = Wad.from_number(20.0) reserve_dai(mcd, c, self.keeper_address, bid_price * tend_lot * 2) simulate_model_output(model=model, price=bid_price, gas_price=10) # and self.start_ignoring_transactions() # and keeper.check_all_auctions() keeper.check_for_bids() # and simulate_model_output(model=model, price=bid_price, gas_price=15) # and self.end_ignoring_transactions() # and keeper.check_for_bids() wait_for_other_threads() # then assert flipper.bids(kick).bid == Rad(bid_price * tend_lot) assert self.web3.eth.getBlock( 'latest', full_transactions=True).transactions[0].gasPrice == 15 # cleanup time_travel_by(self.web3, flipper.ttl() + 1) assert flipper.deal(kick).transact()
def eliminate_queued_debt(cls, web3, mcd, keeper_address): if mcd.vat.sin(mcd.vow.address) == Rad(0): return # given the existence of queued debt c = mcd.collaterals['ETH-A'] kick = c.flipper.kicks() last_bite = mcd.cat.past_bites(10)[0] # when a bid covers the CDP debt auction = c.flipper.bids(kick) reserve_dai(mcd, c, keeper_address, Wad(auction.tab) + Wad(1)) c.flipper.approve( c.flipper.vat(), approval_function=hope_directly(from_address=keeper_address)) c.approve(keeper_address) assert c.flipper.tend( kick, auction.lot, auction.tab).transact(from_address=keeper_address) time_travel_by(web3, c.flipper.ttl() + 1) assert c.flipper.deal(kick).transact() # when a bid covers the vow debt assert mcd.vow.sin_of(last_bite.era(web3)) > Rad(0) assert mcd.vow.flog( last_bite.era(web3)).transact(from_address=keeper_address) assert mcd.vow.heal(mcd.vat.sin(mcd.vow.address)).transact() # then ensure queued debt has been auctioned off assert mcd.vat.sin(mcd.vow.address) == Rad(0)
def test_should_replace_pending_transactions_if_model_raises_bid_and_increases_gas_price( self, kick): # given (model, model_factory) = models(self.keeper, kick) flipper = self.collateral.flipper # when reserve_dai(self.mcd, self.collateral, self.keeper_address, Wad.from_number(35.0) * tend_lot * 2) simulate_model_output(model=model, price=Wad.from_number(15.0), gas_price=10) # and self.start_ignoring_transactions() # and self.keeper.check_all_auctions() self.keeper.check_for_bids() # and self.end_ignoring_transactions() # and simulate_model_output(model=model, price=Wad.from_number(20.0), gas_price=15) # and self.keeper.check_for_bids() wait_for_other_threads() # then assert flipper.bids(kick).bid == Rad(Wad.from_number(20.0) * tend_lot) assert self.web3.eth.getBlock( 'latest', full_transactions=True).transactions[0].gasPrice == 15 # cleanup time_travel_by(self.web3, flipper.ttl() + 1) assert flipper.deal(kick).transact()
def liquidate_urn(cls, web3, mcd, c, gal_address, our_address): # Ensure the CDP isn't safe urn = mcd.vat.urn(c.ilk, gal_address) dart = max_dart(mcd, c, gal_address) - Wad.from_number(1) assert mcd.vat.frob(c.ilk, gal_address, Wad(0), dart).transact(from_address=gal_address) set_collateral_price(mcd, c, Wad.from_number(66)) assert not is_cdp_safe(mcd.vat.ilk(c.ilk.name), urn) # Determine how many bites will be required dunk = Wad(mcd.cat.dunk(c.ilk)) urn = mcd.vat.urn(c.ilk, gal_address) bites_required = math.ceil(urn.art / dunk) print(f"art={urn.art} and dunk={dunk} so {bites_required} bites are required") c.flipper.approve(mcd.vat.address, approval_function=hope_directly(from_address=our_address)) first_kick = c.flipper.kicks() + 1 # Bite and bid on each auction for i in range(bites_required): kick = bite(mcd, c, urn) assert kick > 0 auction = c.flipper.bids(kick) print(f"biting {i} of {bites_required} and bidding tab of {auction.tab}") bid = Wad(auction.tab) + Wad(1) reserve_dai(mcd, c, our_address, bid) print(f"bidding tab of {auction.tab}") assert c.flipper.tend(kick, auction.lot, auction.tab).transact(from_address=our_address) time_travel_by(web3, c.flipper.ttl()) for kick in range(first_kick, c.flipper.kicks()): assert c.flipper.deal(kick).transact() set_collateral_price(mcd, c, Wad.from_number(200)) urn = mcd.vat.urn(c.ilk, gal_address)
def test_should_replace_pending_transactions_if_model_lowers_bid_and_increases_gas_price(self, kick): """ Assuming we want all bids to be submitted as soon as output from the model is parsed, this test seems impractical. In real applications, the model would be unable to submit a lower bid. """ # given (model, model_factory) = models(self.keeper, kick) flipper = self.collateral.flipper assert self.mcd.web3 == self.web3 # when bid_price = Wad.from_number(20.0) reserve_dai(self.mcd, self.collateral, self.keeper_address, bid_price * tend_lot) simulate_model_output(model=model, price=Wad.from_number(20.0), gas_price=10) # and self.start_ignoring_transactions() # and self.keeper.check_all_auctions() self.keeper.check_for_bids() # and self.end_ignoring_transactions() # and simulate_model_output(model=model, price=Wad.from_number(15.0), gas_price=15) # and self.keeper.check_for_bids() wait_for_other_threads() # then assert flipper.bids(kick).bid == Rad(Wad.from_number(15.0) * tend_lot) assert self.web3.eth.getBlock('latest', full_transactions=True).transactions[0].gasPrice == 15 # cleanup time_travel_by(self.web3, flipper.ttl() + 1) assert flipper.deal(kick).transact()
def setup_method(self): self.web3 = web3() self.our_address = our_address(self.web3) self.keeper_address = keeper_address(self.web3) self.other_address = other_address(self.web3) self.gal_address = gal_address(self.web3) self.mcd = mcd(self.web3) self.flopper = self.mcd.flopper self.flopper.approve( self.mcd.vat.address, approval_function=hope_directly(from_address=self.keeper_address)) self.flopper.approve( self.mcd.vat.address, approval_function=hope_directly(from_address=self.other_address)) self.keeper = AuctionKeeper(args=args( f"--eth-from {self.keeper_address} " f"--type flop " f"--from-block 1 " f"--bid-check-interval 0.05 " f"--model ./bogus-model.sh"), web3=self.web3) self.keeper.approve() reserve_dai(self.mcd, self.mcd.collaterals['ETH-C'], self.keeper_address, Wad.from_number(200.00000)) reserve_dai(self.mcd, self.mcd.collaterals['ETH-C'], self.other_address, Wad.from_number(200.00000)) self.sump = self.mcd.vow.sump() # Rad
def simulate_model_bid(self, model, price: Ray, reserve_dai_for_bid=True): assert isinstance(price, Ray) assert price > Ray(0) assert model.id > 0 sale = self.clipper.sales(model.id) assert sale.lot > Wad(0) our_bid = Ray(sale.lot) * price if reserve_dai_for_bid: reserve_dai(self.mcd, self.dai_collateral, self.keeper_address, Wad(our_bid) + Wad(1)) simulate_model_output(model=model, price=Wad(price))
def tend_with_dai(mcd: DssDeployment, c: Collateral, flipper: Flipper, id: int, address: Address, bid: Rad): assert (isinstance(mcd, DssDeployment)) assert (isinstance(c, Collateral)) assert (isinstance(flipper, Flipper)) assert (isinstance(id, int)) assert (isinstance(bid, Rad)) flipper.approve(flipper.vat(), approval_function=hope_directly(from_address=address)) previous_bid = flipper.bids(id) c.approve(address) reserve_dai(mcd, c, address, Wad(bid), extra_collateral=Wad.from_number(2)) TestAuctionKeeperFlipper.tend(flipper, id, address, previous_bid.lot, bid)
def simulate_model_bid(self, mcd: DssDeployment, c: Collateral, model: object, price: Wad, gas_price: Optional[int] = None): assert (isinstance(mcd, DssDeployment)) assert (isinstance(c, Collateral)) assert (isinstance(price, Wad)) assert (isinstance(gas_price, int)) or gas_price is None assert price > Wad(0) flipper = c.flipper initial_bid = flipper.bids(model.id) assert initial_bid.lot > Wad(0) our_bid = price * initial_bid.lot reserve_dai(mcd, c, self.keeper_address, our_bid, extra_collateral=Wad.from_number(2)) simulate_model_output(model=model, price=price, gas_price=gas_price)
def test_should_provide_model_with_updated_info_after_our_own_bid( self, mcd, c, gal_address, keeper): # given flipper = c.flipper kick = flipper.kicks() (model, model_factory) = models(keeper, kick) # when keeper.check_all_auctions() wait_for_other_threads() previous_bid = flipper.bids(model.id) # then assert model.send_status.call_count == 1 # when initial_bid = flipper.bids(kick) our_price = Wad.from_number(30) our_bid = our_price * initial_bid.lot reserve_dai(mcd, c, self.keeper_address, our_bid) simulate_model_output(model=model, price=our_price) keeper.check_for_bids() # and keeper.check_all_auctions() wait_for_other_threads() # and keeper.check_all_auctions() wait_for_other_threads() # then assert model.send_status.call_count > 1 # and status = model.send_status.call_args[0][0] assert status.id == kick assert status.flipper == flipper.address assert status.flapper is None assert status.flopper is None assert status.bid == Rad(our_price * status.lot) assert status.lot == previous_bid.lot assert status.tab == previous_bid.tab assert status.beg > Wad.from_number(1) assert status.guy == self.keeper_address assert status.era > 0 assert status.end > status.era assert status.tic > status.era assert status.price == our_price # cleanup time_travel_by(self.web3, flipper.ttl() + 1) assert flipper.deal(kick).transact()
def take_with_dai(self, id: int, price: Ray, address: Address): assert isinstance(id, int) assert isinstance(price, Ray) assert isinstance(address, Address) lot = self.clipper.sales(id).lot assert lot > Wad(0) cost = Wad(price * Ray(lot)) logging.debug(f"reserving {cost} Dai to bid on auction {id}") reserve_dai(self.mcd, self.dai_collateral, address, cost) assert self.mcd.vat.dai(address) >= Rad(cost) logging.debug(f"attempting to take clip {id} at {price}") self.clipper.validate_take(id, lot, price, address) assert self.clipper.take(id, lot, price, address).transact(from_address=address) # confirm that take finished the auction (needs_redo, auction_price, lot, tab) = self.clipper.status(id) assert not needs_redo assert lot == Wad(0) or tab == Rad(0)
def test_should_take_after_someone_else_took(self, kick): # given (model, model_factory) = models(self.keeper, kick) sale = self.clipper.sales(kick) assert sale.lot == Wad.from_number(1) # when another actor took most of the lot time_travel_by(self.web3, 12) sale = self.clipper.sales(kick) (needs_redo, price, lot, tab) = self.clipper.status(kick) their_amt = Wad.from_number(0.6) their_bid = Wad(Ray(their_amt) * price) assert Rad(their_bid) < sale.tab # ensure some collateral will be left over reserve_dai(self.mcd, self.dai_collateral, self.other_address, their_bid) self.clipper.validate_take(kick, their_amt, price, self.other_address) assert self.clipper.take(kick, their_amt, price, self.other_address).transact(from_address=self.other_address) sale = self.clipper.sales(kick) assert sale.lot > Wad(0) # and our model is configured to bid a few seconds into the auction sale = self.clipper.sales(kick) (needs_redo, price, lot, tab) = self.clipper.status(kick) assert Rad(price) > sale.tab # pad our bid to ensure decimal precision doesn't cause it to be thrown away self.simulate_model_bid(model, price + Ray(Wad(1))) self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then ensure our take finished the auction our_take = self.last_log() assert isinstance(our_take, Clipper.TakeLog) assert our_take.id == kick assert Wad(0) < our_take.lot <= lot (needs_redo, price, lot, tab) = self.clipper.status(kick) assert not needs_redo assert lot == Wad(0) or tab == Rad(0)
def purchase_dai(self, amount: Wad): assert isinstance(amount, Wad) seller = self.our_address reserve_dai(self.mcd, self.mcd.collaterals['ETH-C'], seller, amount) assert self.mcd.dai_adapter.exit(seller, amount).transact(from_address=seller) assert self.mcd.dai.transfer_from(seller, self.keeper_address, amount).transact(from_address=seller)
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import sys from pymaker.numeric import Wad, Ray, Rad from tests.conftest import keeper_address, mcd, other_address, reserve_dai, web3 mcd = mcd(web3()) collateral = mcd.collaterals['ETH-C'] keeper_address = keeper_address(web3()) seller = other_address(web3()) amount = Wad.from_number(float(sys.argv[1])) assert amount > Wad(0) web3().eth.defaultAccount = seller.address collateral.approve(seller) mcd.approve_dai(seller) reserve_dai(mcd, mcd.collaterals['ETH-C'], seller, amount, Wad.from_number(2)) assert mcd.dai_adapter.exit(seller, amount).transact(from_address=seller) assert mcd.dai.transfer_from(seller, keeper_address, amount).transact(from_address=seller) print( f'Purchased {str(amount)} Dai, keeper token balance is {str(mcd.dai.balance_of(keeper_address))}' )