class TestAuctionKeeperFlapper(TransactionIgnoringTest): 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.flapper = self.mcd.flapper self.flapper.approve(self.mcd.mkr.address, directly(from_address=self.other_address)) self.keeper = AuctionKeeper(args=args(f"--eth-from {self.keeper_address} " f"--type flap " f"--from-block 1 " f"--model ./bogus-model.sh"), web3=self.web3) self.keeper.approve() mint_mkr(self.mcd.mkr, self.keeper_address, Wad.from_number(50000)) mint_mkr(self.mcd.mkr, self.other_address, Wad.from_number(50000)) assert isinstance(self.keeper.gas_price, DynamicGasPrice) # Since no args were assigned, gas strategy should return a GeometricGasPrice starting at the node gas price self.default_gas_price = get_node_gas_price(self.web3) def test_should_detect_flap(self, web3, mcd, c, gal_address, keeper_address): # given some MKR is available to the keeper and a count of flap auctions mint_mkr(mcd.mkr, keeper_address, Wad.from_number(50000)) kicks = mcd.flapper.kicks() # when surplus is generated create_cdp_with_surplus(mcd, c, gal_address) self.keeper.check_flap() wait_for_other_threads() # then ensure another flap auction was kicked off kick = mcd.flapper.kicks() assert kick == kicks + 1 # clean up by letting someone else bid and waiting until the auction ends auction = self.flapper.bids(kick) assert self.flapper.tend(kick, auction.lot, Wad.from_number(30)).transact(from_address=self.other_address) time_travel_by(web3, mcd.flapper.ttl() + 1) def test_should_start_a_new_model_and_provide_it_with_info_on_auction_kick(self, kick): # given (model, model_factory) = models(self.keeper, kick) # when self.keeper.check_all_auctions() wait_for_other_threads() # then model_factory.create_model.assert_called_once_with(Parameters(auction_contract=self.keeper.mcd.flapper, id=kick)) # and status = model.send_status.call_args[0][0] assert status.id == kick assert status.flipper is None assert status.flapper == self.flapper.address assert status.flopper is None assert status.bid == Wad(0) assert status.lot == self.mcd.vow.bump() assert status.tab is None assert status.beg == Wad.from_number(1.05) assert status.guy == self.mcd.vow.address assert status.era > 0 assert status.end < status.era + self.flapper.tau() + 1 assert status.tic == 0 assert status.price is None def test_should_provide_model_with_updated_info_after_our_own_bid(self): # given kick = self.flapper.kicks() (model, model_factory) = models(self.keeper, kick) # when self.keeper.check_all_auctions() wait_for_other_threads() # then assert model.send_status.call_count == 1 # when simulate_model_output(model=model, price=Wad.from_number(9)) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # and self.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 is None assert status.flapper == self.flapper.address assert status.flopper is None assert status.bid == Wad(self.flapper.bids(kick).lot / Rad.from_number(9)) assert status.lot == self.mcd.vow.bump() assert status.tab is None assert status.beg == Wad.from_number(1.05) assert status.guy == self.keeper_address assert status.era > 0 assert status.end > status.era assert status.tic > status.era assert round(status.price, 2) == round(Wad.from_number(9), 2) # cleanup time_travel_by(self.web3, self.flapper.ttl() + 1) assert self.flapper.deal(kick).transact() def test_should_provide_model_with_updated_info_after_somebody_else_bids(self, kick): # given (model, model_factory) = models(self.keeper, kick) # when self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert model.send_status.call_count == 1 # when auction = self.flapper.bids(kick) assert Wad.from_number(40) > auction.bid assert self.flapper.tend(kick, auction.lot, Wad.from_number(40)).transact(from_address=self.other_address) # and self.keeper.check_all_auctions() wait_for_other_threads() auction = self.flapper.bids(kick) # then assert model.send_status.call_count > 1 # and status = model.send_status.call_args[0][0] assert status.id == kick assert status.flipper is None assert status.flapper == self.flapper.address assert status.flopper is None assert status.bid == Wad.from_number(40) assert status.lot == auction.lot assert status.tab is None assert status.beg == Wad.from_number(1.05) assert status.guy == self.other_address assert status.era > 0 assert status.end > status.era assert status.tic > status.era assert status.price == Wad(auction.lot / Rad(auction.bid)) # cleanup time_travel_by(self.web3, self.flapper.ttl() + 1) assert self.flapper.deal(kick).transact() def test_should_tick_if_auction_expired_due_to_tau(self, kick): # given (model, model_factory) = models(self.keeper, kick) # when self.keeper.check_all_auctions() wait_for_other_threads() # then model_factory.create_model.assert_called_once() model.terminate.assert_not_called() # when time_travel_by(self.web3, self.flapper.tau() + 1) # and simulate_model_output(model=model, price=Wad.from_number(9.0)) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then model.terminate.assert_not_called() auction = self.flapper.bids(kick) assert round(Wad(auction.lot) / auction.bid, 2) == round(Wad.from_number(9.0), 2) # cleanup time_travel_by(self.web3, self.flapper.ttl() + 1) model_factory.create_model.assert_called_once() self.keeper.check_all_auctions() model.terminate.assert_called_once() def test_should_terminate_model_if_auction_expired_due_to_ttl_and_somebody_else_won_it(self, kick): # given (model, model_factory) = models(self.keeper, kick) # when self.keeper.check_all_auctions() wait_for_other_threads() # then model_factory.create_model.assert_called_once() model.terminate.assert_not_called() # when auction = self.flapper.bids(kick) assert self.flapper.tend(kick, auction.lot, Wad.from_number(40)).transact(from_address=self.other_address) # and time_travel_by(self.web3, self.flapper.ttl() + 1) # and self.keeper.check_all_auctions() wait_for_other_threads() # then model_factory.create_model.assert_called_once() model.terminate.assert_called_once() def test_should_terminate_model_if_auction_is_dealt(self, kick): # given (model, model_factory) = models(self.keeper, kick) # when self.keeper.check_all_auctions() wait_for_other_threads() # then model_factory.create_model.assert_called_once() model.terminate.assert_not_called() # when auction = self.flapper.bids(kick) assert self.flapper.tend(kick, auction.lot, Wad.from_number(40)).transact(from_address=self.other_address) # and time_travel_by(self.web3, self.flapper.ttl() + 1) # and assert self.flapper.deal(kick).transact(from_address=self.other_address) # and self.keeper.check_all_auctions() wait_for_other_threads() # then model_factory.create_model.assert_called_once() model.terminate.assert_called_once() def test_should_not_instantiate_model_if_auction_is_dealt(self, kick): # given (model, model_factory) = models(self.keeper, kick) # and auction = self.flapper.bids(kick) self.flapper.tend(kick, auction.lot, Wad.from_number(40)).transact(from_address=self.other_address) # and time_travel_by(self.web3, self.flapper.ttl() + 1) # and self.flapper.deal(kick).transact(from_address=self.other_address) # when self.keeper.check_all_auctions() wait_for_other_threads() # then model_factory.create_model.assert_not_called() def test_should_not_do_anything_if_no_output_from_model(self, kick): # given assert kick previous_block_number = self.web3.eth.blockNumber # when # [no output from model] # and self.keeper.check_all_auctions() wait_for_other_threads() # then assert self.web3.eth.blockNumber == previous_block_number def test_should_make_initial_bid(self): # given kick = self.flapper.kicks() (model, model_factory) = models(self.keeper, kick) # when simulate_model_output(model=model, price=Wad.from_number(10.0)) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then auction = self.flapper.bids(kick) assert round(Wad(auction.lot) / auction.bid, 2) == round(Wad.from_number(10.0), 2) # cleanup time_travel_by(self.web3, self.flapper.ttl() + 1) assert self.flapper.deal(kick).transact() def test_should_bid_even_if_there_is_already_a_bidder(self, kick): # given (model, model_factory) = models(self.keeper, kick) # and auction = self.flapper.bids(kick) assert self.flapper.tend(kick, auction.lot, Wad.from_number(16)).transact(from_address=self.other_address) assert self.flapper.bids(kick).bid == Wad.from_number(16) # when simulate_model_output(model=model, price=Wad.from_number(0.0000005)) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then auction = self.flapper.bids(kick) assert round(Wad(auction.lot) / auction.bid, 2) == round(Wad.from_number(0.0000005), 2) # cleanup time_travel_by(self.web3, self.flapper.ttl() + 1) assert self.flapper.deal(kick).transact() def test_should_outbid_a_zero_bid(self, kick): # given (model, model_factory) = models(self.keeper, kick) # and auction = self.flapper.bids(kick) assert self.flapper.tend(kick, auction.lot, Wad(1)).transact(from_address=self.other_address) assert self.flapper.bids(kick).bid == Wad(1) # when simulate_model_output(model=model, price=Wad.from_number(0.0000006)) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then auction = self.flapper.bids(kick) assert round(Wad(auction.lot) / auction.bid, 2) == round(Wad.from_number(0.0000006), 2) # cleanup time_travel_by(self.web3, self.flapper.ttl() + 1) assert self.flapper.deal(kick).transact() def test_should_overbid_itself_if_model_has_updated_the_price(self, kick): # given (model, model_factory) = models(self.keeper, kick) lot = self.flapper.bids(kick).lot # when first_bid = Wad.from_number(0.0000004) simulate_model_output(model=model, price=first_bid) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert self.flapper.bids(kick).bid == Wad(lot / Rad(first_bid)) # when second_bid = Wad.from_number(0.0000003) simulate_model_output(model=model, price=second_bid) self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert self.flapper.bids(kick).bid == Wad(lot / Rad(second_bid)) # cleanup time_travel_by(self.web3, self.flapper.ttl() + 1) assert self.flapper.deal(kick).transact() def test_should_increase_gas_price_of_pending_transactions_if_model_increases_gas_price(self, kick): # given (model, model_factory) = models(self.keeper, kick) lot = self.flapper.bids(kick).lot # when simulate_model_output(model=model, price=Wad.from_number(10.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(10.0), gas_price=15) # and self.keeper.check_for_bids() wait_for_other_threads() # then assert self.flapper.bids(kick).bid == Wad(lot) / Wad.from_number(10.0) assert self.web3.eth.getBlock('latest', full_transactions=True).transactions[0].gasPrice == 15 # cleanup time_travel_by(self.web3, self.flapper.ttl() + 1) assert self.flapper.deal(kick).transact() def test_should_replace_pending_transactions_if_model_raises_bid_and_increases_gas_price(self, kick): # given (model, model_factory) = models(self.keeper, kick) lot = self.flapper.bids(kick).lot # when simulate_model_output(model=model, price=Wad.from_number(9.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(8.0), gas_price=15) # and self.keeper.check_for_bids() wait_for_other_threads() # then assert round(self.flapper.bids(kick).bid, 2) == round(Wad(lot / Rad.from_number(8.0)), 2) assert self.web3.eth.getBlock('latest', full_transactions=True).transactions[0].gasPrice == 15 # cleanup time_travel_by(self.web3, self.flapper.ttl() + 1) assert self.flapper.deal(kick).transact() def test_should_replace_pending_transactions_if_model_lowers_bid_and_increases_gas_price(self, kick): # given (model, model_factory) = models(self.keeper, kick) lot = self.flapper.bids(kick).lot # when simulate_model_output(model=model, price=Wad.from_number(10.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(8.0), gas_price=15) # and self.keeper.check_for_bids() wait_for_other_threads() # then assert self.flapper.bids(kick).bid == Wad(lot) / Wad.from_number(8.0) assert self.web3.eth.getBlock('latest', full_transactions=True).transactions[0].gasPrice == 15 # cleanup time_travel_by(self.web3, self.flapper.ttl() + 1) assert self.flapper.deal(kick).transact() def test_should_not_bid_on_rounding_errors_with_small_amounts(self, kick): # given (model, model_factory) = models(self.keeper, kick) lot = self.flapper.bids(kick).lot # when price = Wad.from_number(9.0)-Wad(5) simulate_model_output(model=model, price=price) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert self.flapper.bids(kick).bid == Wad(lot) / Wad(price) # when tx_count = self.web3.eth.getTransactionCount(self.keeper_address.address) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert self.web3.eth.getTransactionCount(self.keeper_address.address) == tx_count # cleanup time_travel_by(self.web3, self.flapper.ttl() + 1) assert self.flapper.deal(kick).transact() def test_should_deal_when_we_won_the_auction(self, kick): # given (model, model_factory) = models(self.keeper, kick) # when simulate_model_output(model=model, price=Wad.from_number(8.0)) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then auction = self.flapper.bids(kick) assert auction.bid > Wad(0) assert round(Wad(auction.lot) / auction.bid, 2) == round(Wad.from_number(8.0), 2) dai_before = self.mcd.vat.dai(self.keeper_address) # when time_travel_by(self.web3, self.flapper.ttl() + 1) # and self.keeper.check_all_auctions() wait_for_other_threads() dai_after = self.mcd.vat.dai(self.keeper_address) # then assert dai_before < dai_after def test_should_not_deal_when_auction_finished_but_somebody_else_won(self, kick): # given (model, model_factory) = models(self.keeper, kick) lot = self.flapper.bids(kick).lot # and assert self.flapper.tend(kick, lot, Wad.from_number(16)).transact(from_address=self.other_address) assert self.flapper.bids(kick).bid == Wad.from_number(16) # when time_travel_by(self.web3, self.flapper.ttl() + 1) # and self.keeper.check_all_auctions() wait_for_other_threads() # then assert self.flapper.bids(kick).bid == Wad.from_number(16) def test_should_obey_gas_price_provided_by_the_model(self, kick): # given (model, model_factory) = models(self.keeper, kick) # when simulate_model_output(model=model, price=Wad.from_number(10.0), gas_price=175000) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert self.web3.eth.getBlock('latest', full_transactions=True).transactions[0].gasPrice == 175000 # cleanup time_travel_by(self.web3, self.flapper.ttl() + 1) assert self.flapper.deal(kick).transact() def test_should_use_default_gas_price_if_not_provided_by_the_model(self, kick): # given (model, model_factory) = models(self.keeper, kick) # when simulate_model_output(model=model, price=Wad.from_number(10.0)) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then auction = self.flapper.bids(kick) assert auction.guy == self.keeper_address assert auction.bid > Wad(0) assert self.web3.eth.getBlock('latest', full_transactions=True).transactions[0].gasPrice == \ self.default_gas_price print(f"tx gas price is {self.web3.eth.getBlock('latest', full_transactions=True).transactions[0].gasPrice}, " f"web3.eth.gasPrice is {self.web3.eth.gasPrice}") # cleanup time_travel_by(self.web3, self.flapper.ttl() + 1) assert self.flapper.deal(kick).transact() def test_should_change_gas_strategy_when_model_output_changes(self, kick): # given (model, model_factory) = models(self.keeper, kick) lot = self.flapper.bids(kick).lot # when first_bid = Wad.from_number(0.0000009) simulate_model_output(model=model, price=first_bid, gas_price=2000) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert self.web3.eth.getBlock('latest', full_transactions=True).transactions[0].gasPrice == 2000 # when second_bid = Wad.from_number(0.0000006) simulate_model_output(model=model, price=second_bid) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert self.flapper.bids(kick).bid == Wad(lot / Rad(second_bid)) assert self.web3.eth.getBlock('latest', full_transactions=True).transactions[0].gasPrice == \ self.default_gas_price # when third_bid = Wad.from_number(0.0000003) new_gas_price = int(self.default_gas_price*1.25) simulate_model_output(model=model, price=third_bid, gas_price=new_gas_price) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert self.flapper.bids(kick).bid == Wad(lot / Rad(third_bid)) assert self.web3.eth.getBlock('latest', full_transactions=True).transactions[0].gasPrice == new_gas_price # cleanup time_travel_by(self.web3, self.flapper.ttl() + 1) assert self.flapper.deal(kick).transact() @classmethod def teardown_class(cls): cls.mcd = mcd(web3()) if not repay_urn(cls.mcd, c(cls.mcd), gal_address(web3())): liquidate_urn(cls.mcd, c(cls.mcd), gal_address(web3()), keeper_address(web3())) kill_other_threads()
class TestAuctionKeeperFlapper(TransactionIgnoringTest): 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.flapper = self.mcd.flapper self.flapper.approve(self.mcd.mkr.address, directly(from_address=self.other_address)) self.keeper = AuctionKeeper(args=args( f"--eth-from {self.keeper_address} " f"--type flap " f"--network testnet " f"--model ./bogus-model.sh"), web3=self.web3) self.keeper.approve() mint_mkr(self.mcd.mkr, self.keeper_address, Wad.from_number(50000)) mint_mkr(self.mcd.mkr, self.other_address, Wad.from_number(50000)) def test_should_detect_flap(self, web3, mcd, c, gal_address, keeper_address): # given some MKR is available to the keeper and a count of flap auctions mint_mkr(mcd.mkr, keeper_address, Wad.from_number(50000)) kicks = mcd.flapper.kicks() # when surplus is generated create_cdp_with_surplus(mcd, c, gal_address) self.keeper.check_flap() wait_for_other_threads() # then ensure another flap auction was kicked off assert mcd.flapper.kicks() == kicks + 1 # clean up by letting the auction expire time_travel_by(web3, mcd.flapper.tau() + 1) def test_should_start_a_new_model_and_provide_it_with_info_on_auction_kick( self, kick): # given (model, model_factory) = models(self.keeper, kick) # when self.keeper.check_all_auctions() wait_for_other_threads() # then model_factory.create_model.assert_called_once_with( Parameters(flipper=None, flapper=self.flapper.address, flopper=None, id=kick)) # and status = model.send_status.call_args[0][0] assert status.id == kick assert status.flipper is None assert status.flapper == self.flapper.address assert status.flopper is None assert status.bid == Wad(0) assert status.lot == self.mcd.vow.bump() assert status.tab is None assert status.beg == Ray.from_number(1.05) assert status.guy == self.mcd.vow.address assert status.era > 0 assert status.end < status.era + self.flapper.tau() + 1 assert status.tic == 0 assert status.price is None def test_should_provide_model_with_updated_info_after_our_own_bid(self): # given kick = self.flapper.kicks() (model, model_factory) = models(self.keeper, kick) # when self.keeper.check_all_auctions() wait_for_other_threads() # then assert model.send_status.call_count == 1 # when simulate_model_output(model=model, price=Wad.from_number(9)) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # and self.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 is None assert status.flapper == self.flapper.address assert status.flopper is None assert status.bid == Wad( self.flapper.bids(kick).lot / Rad.from_number(9)) assert status.lot == self.mcd.vow.bump() assert status.tab is None assert status.beg == Ray.from_number(1.05) assert status.guy == self.keeper_address assert status.era > 0 assert status.end > status.era assert status.tic > status.era assert round(status.price, 2) == round(Wad.from_number(9), 2) # cleanup time_travel_by(self.web3, self.flapper.ttl() + 1) assert self.flapper.deal(kick).transact() def test_should_provide_model_with_updated_info_after_somebody_else_bids( self, kick): # given (model, model_factory) = models(self.keeper, kick) # when self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert model.send_status.call_count == 1 # when auction = self.flapper.bids(kick) assert Wad.from_number(40) > auction.bid assert self.flapper.tend( kick, auction.lot, Wad.from_number(40)).transact(from_address=self.other_address) # and self.keeper.check_all_auctions() wait_for_other_threads() auction = self.flapper.bids(kick) # then assert model.send_status.call_count > 1 # and status = model.send_status.call_args[0][0] assert status.id == kick assert status.flipper is None assert status.flapper == self.flapper.address assert status.flopper is None assert status.bid == Wad.from_number(40) assert status.lot == auction.lot assert status.tab is None assert status.beg == Ray.from_number(1.05) assert status.guy == self.other_address assert status.era > 0 assert status.end > status.era assert status.tic > status.era assert status.price == Wad(auction.lot / Rad(auction.bid)) # cleanup time_travel_by(self.web3, self.flapper.ttl() + 1) assert self.flapper.deal(kick).transact() def test_should_terminate_model_if_auction_expired_due_to_tau(self, kick): # given (model, model_factory) = models(self.keeper, kick) # when self.keeper.check_all_auctions() wait_for_other_threads() # then model_factory.create_model.assert_called_once() model.terminate.assert_not_called() # when time_travel_by(self.web3, self.flapper.tau() + 1) # and self.keeper.check_all_auctions() wait_for_other_threads() # then model_factory.create_model.assert_called_once() model.terminate.assert_called_once() def test_should_terminate_model_if_auction_expired_due_to_ttl_and_somebody_else_won_it( self, kick): # given (model, model_factory) = models(self.keeper, kick) # when self.keeper.check_all_auctions() wait_for_other_threads() # then model_factory.create_model.assert_called_once() model.terminate.assert_not_called() # when auction = self.flapper.bids(kick) assert self.flapper.tend( kick, auction.lot, Wad.from_number(40)).transact(from_address=self.other_address) # and time_travel_by(self.web3, self.flapper.ttl() + 1) # and self.keeper.check_all_auctions() wait_for_other_threads() # then model_factory.create_model.assert_called_once() model.terminate.assert_called_once() def test_should_terminate_model_if_auction_is_dealt(self, kick): # given kick = self.flapper.kicks() (model, model_factory) = models(self.keeper, kick) # when self.keeper.check_all_auctions() wait_for_other_threads() # then model_factory.create_model.assert_called_once() model.terminate.assert_not_called() # when auction = self.flapper.bids(kick) assert self.flapper.tend( kick, auction.lot, Wad.from_number(40)).transact(from_address=self.other_address) # and time_travel_by(self.web3, self.flapper.ttl() + 1) # and assert self.flapper.deal(kick).transact( from_address=self.other_address) # and self.keeper.check_all_auctions() wait_for_other_threads() # then model_factory.create_model.assert_called_once() model.terminate.assert_called_once() def test_should_not_instantiate_model_if_auction_is_dealt(self, kick): # given (model, model_factory) = models(self.keeper, kick) # and auction = self.flapper.bids(kick) self.flapper.tend( kick, auction.lot, Wad.from_number(40)).transact(from_address=self.other_address) # and time_travel_by(self.web3, self.flapper.ttl() + 1) # and self.flapper.deal(kick).transact(from_address=self.other_address) # when self.keeper.check_all_auctions() wait_for_other_threads() # then model_factory.create_model.assert_not_called() def test_should_not_do_anything_if_no_output_from_model(self, kick): # given previous_block_number = self.web3.eth.blockNumber # when # [no output from model] # and self.keeper.check_all_auctions() wait_for_other_threads() # then assert self.web3.eth.blockNumber == previous_block_number def test_should_make_initial_bid(self): # given kick = self.flapper.kicks() (model, model_factory) = models(self.keeper, kick) # when simulate_model_output(model=model, price=Wad.from_number(10.0)) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then auction = self.flapper.bids(kick) assert round(Wad(auction.lot) / auction.bid, 2) == round(Wad.from_number(10.0), 2) # cleanup time_travel_by(self.web3, self.flapper.ttl() + 1) assert self.flapper.deal(kick).transact() def test_should_bid_even_if_there_is_already_a_bidder(self, kick): # given (model, model_factory) = models(self.keeper, kick) # and auction = self.flapper.bids(kick) assert self.flapper.tend( kick, auction.lot, Wad.from_number(16)).transact(from_address=self.other_address) assert self.flapper.bids(kick).bid == Wad.from_number(16) # when simulate_model_output(model=model, price=Wad.from_number(0.0000005)) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then auction = self.flapper.bids(kick) assert round(Wad(auction.lot) / auction.bid, 2) == round(Wad.from_number(0.0000005), 2) # cleanup time_travel_by(self.web3, self.flapper.ttl() + 1) assert self.flapper.deal(kick).transact() def test_should_overbid_itself_if_model_has_updated_the_price(self, kick): # given (model, model_factory) = models(self.keeper, kick) lot = self.flapper.bids(kick).lot # when first_bid = Wad.from_number(0.0000004) simulate_model_output(model=model, price=first_bid) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert self.flapper.bids(kick).bid == Wad(lot / Rad(first_bid)) # when second_bid = Wad.from_number(0.0000003) simulate_model_output(model=model, price=second_bid) self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert self.flapper.bids(kick).bid == Wad(lot / Rad(second_bid)) # cleanup time_travel_by(self.web3, self.flapper.ttl() + 1) assert self.flapper.deal(kick).transact() def test_should_increase_gas_price_of_pending_transactions_if_model_increases_gas_price( self, kick): # given (model, model_factory) = models(self.keeper, kick) lot = self.flapper.bids(kick).lot # when simulate_model_output(model=model, price=Wad.from_number(10.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(10.0), gas_price=15) # and self.keeper.check_for_bids() wait_for_other_threads() # then assert self.flapper.bids(kick).bid == Wad(lot) / Wad.from_number(10.0) assert self.web3.eth.getBlock( 'latest', full_transactions=True).transactions[0].gasPrice == 15 # cleanup time_travel_by(self.web3, self.flapper.ttl() + 1) assert self.flapper.deal(kick).transact() def test_should_replace_pending_transactions_if_model_raises_bid_and_increases_gas_price( self, kick): # given (model, model_factory) = models(self.keeper, kick) lot = self.flapper.bids(kick).lot # when simulate_model_output(model=model, price=Wad.from_number(9.0), gas_price=10) # and self.start_ignoring_transactions() # and self.keeper.check_all_auctions() self.keeper.check_for_bids() # and time.sleep(2) # and self.end_ignoring_transactions() # and simulate_model_output(model=model, price=Wad.from_number(8.0), gas_price=15) # and self.keeper.check_for_bids() wait_for_other_threads() # then assert round(self.flapper.bids(kick).bid, 2) == round(Wad(lot / Rad.from_number(8.0)), 2) assert self.web3.eth.getBlock( 'latest', full_transactions=True).transactions[0].gasPrice == 15 # cleanup time_travel_by(self.web3, self.flapper.ttl() + 1) assert self.flapper.deal(kick).transact() @pytest.mark.skip( "complexities replacing the transaction need to be sorted") def test_should_replace_pending_transactions_if_model_lowers_bid_and_increases_gas_price( self, kick): # given (model, model_factory) = models(self.keeper, kick) lot = self.flapper.bids(kick).lot # when simulate_model_output(model=model, price=Wad.from_number(10.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(8.0), gas_price=15) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert self.flapper.bids(kick).bid == Wad(lot) / Wad.from_number(8.0) assert self.web3.eth.getBlock( 'latest', full_transactions=True).transactions[0].gasPrice == 15 # cleanup time_travel_by(self.web3, self.flapper.ttl() + 1) assert self.flapper.deal(kick).transact() def test_should_not_bid_on_rounding_errors_with_small_amounts(self, kick): # given (model, model_factory) = models(self.keeper, kick) lot = self.flapper.bids(kick).lot # when price = Wad.from_number(9.0) - Wad(5) simulate_model_output(model=model, price=price) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert self.flapper.bids(kick).bid == Wad(lot) / Wad(price) # when tx_count = self.web3.eth.getTransactionCount( self.keeper_address.address) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert self.web3.eth.getTransactionCount( self.keeper_address.address) == tx_count # cleanup time_travel_by(self.web3, self.flapper.ttl() + 1) assert self.flapper.deal(kick).transact() def test_should_deal_when_we_won_the_auction(self, kick): # given (model, model_factory) = models(self.keeper, kick) # when simulate_model_output(model=model, price=Wad.from_number(8.0)) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then auction = self.flapper.bids(kick) assert auction.bid > Wad(0) assert round(Wad(auction.lot) / auction.bid, 2) == round(Wad.from_number(8.0), 2) dai_before = self.mcd.vat.dai(self.keeper_address) # when time_travel_by(self.web3, self.flapper.ttl() + 1) # and self.keeper.check_all_auctions() wait_for_other_threads() dai_after = self.mcd.vat.dai(self.keeper_address) # then assert dai_before < dai_after def test_should_not_deal_when_auction_finished_but_somebody_else_won( self, kick): # given (model, model_factory) = models(self.keeper, kick) lot = self.flapper.bids(kick).lot # and assert self.flapper.tend( kick, lot, Wad.from_number(16)).transact(from_address=self.other_address) assert self.flapper.bids(kick).bid == Wad.from_number(16) # when time_travel_by(self.web3, self.flapper.ttl() + 1) # and self.keeper.check_all_auctions() wait_for_other_threads() # then assert self.flapper.bids(kick).bid == Wad.from_number(16) def test_should_obey_gas_price_provided_by_the_model(self, kick): # given (model, model_factory) = models(self.keeper, kick) # when simulate_model_output(model=model, price=Wad.from_number(10.0), gas_price=175000) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert self.web3.eth.getBlock( 'latest', full_transactions=True).transactions[0].gasPrice == 175000 def test_should_use_default_gas_price_if_not_provided_by_the_model(self): # given kick = self.flapper.kicks() (model, model_factory) = models(self.keeper, kick) # when simulate_model_output(model=model, price=Wad.from_number(10.0)) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert self.web3.eth.getBlock('latest', full_transactions=True).transactions[0].gasPrice == \ self.web3.eth.gasPrice # cleanup time_travel_by(self.web3, self.flapper.ttl() + 1) assert self.flapper.deal(kick).transact() @classmethod def teardown_class(cls): cls.mcd = mcd(web3()) cls.liquidate_urn(web3(), cls.mcd, c(cls.mcd), gal_address(web3()), our_address(web3())) @classmethod 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)
class TestAuctionKeeperFlopper(TransactionIgnoringTest): 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 dent(self, id: int, address: Address, lot: Wad, bid: Rad): assert (isinstance(id, int)) assert (isinstance(lot, Wad)) assert (isinstance(bid, Rad)) assert self.flopper.live() == 1 current_bid = self.flopper.bids(id) assert current_bid.guy != Address( "0x0000000000000000000000000000000000000000") assert current_bid.tic > datetime.now().timestamp( ) or current_bid.tic == 0 assert current_bid.end > datetime.now().timestamp() assert bid == current_bid.bid assert Wad(0) < lot < current_bid.lot assert self.flopper.beg() * lot <= current_bid.lot assert self.flopper.dent(id, lot, bid).transact(from_address=address) def lot_implies_price(self, kick: int, price: Wad) -> bool: return round(Rad(self.flopper.bids(kick).lot), 2) == round(self.sump / Rad(price), 2) 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_start_a_new_model_and_provide_it_with_info_on_auction_kick( self, kick): # given (model, model_factory) = models(self.keeper, kick) # when self.keeper.check_all_auctions() wait_for_other_threads() # then model_factory.create_model.assert_called_once_with( Parameters(flipper=None, flapper=None, flopper=self.flopper.address, id=kick)) # and status = model.send_status.call_args[0][0] assert status.id == kick assert status.flipper is None assert status.flapper is None assert status.flopper == self.flopper.address assert status.bid > Rad.from_number(0) assert status.lot == self.mcd.vow.dump() assert status.tab is None assert status.beg > Wad.from_number(1) assert status.guy == self.mcd.vow.address assert status.era > 0 assert status.end < status.era + self.flopper.tau() + 1 assert status.tic == 0 assert status.price == Wad(status.bid / Rad(status.lot)) def test_should_provide_model_with_updated_info_after_our_own_bid(self): # given kick = self.flopper.kicks() (model, model_factory) = models(self.keeper, kick) # when self.keeper.check_all_auctions() wait_for_other_threads() # then assert model.send_status.call_count == 1 # when price = Wad.from_number(50.0) simulate_model_output(model=model, price=price) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # and self.keeper.check_all_auctions() wait_for_other_threads() # then assert model.send_status.call_count > 1 last_bid = self.flopper.bids(kick) # and status = model.send_status.call_args[0][0] assert status.id == kick assert status.flipper is None assert status.flapper is None assert status.flopper == self.flopper.address assert status.bid == last_bid.bid assert status.lot == Wad(last_bid.bid / Rad(price)) assert status.tab is None 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 == price # cleanup time_travel_by(self.web3, self.flopper.ttl() + 1) assert self.flopper.deal(kick).transact() def test_should_provide_model_with_updated_info_after_somebody_else_bids( self, kick): # given (model, model_factory) = models(self.keeper, kick) # when self.keeper.check_all_auctions() wait_for_other_threads() # then assert model.send_status.call_count == 1 # when lot = Wad.from_number(0.0000001) assert self.flopper.dent( kick, lot, self.sump).transact(from_address=self.other_address) # and self.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 is None assert status.flapper is None assert status.flopper == self.flopper.address assert status.bid == self.sump assert status.lot == lot assert status.tab is None assert status.beg > Wad.from_number(1) assert status.guy == self.other_address assert status.era > 0 assert status.end > status.era assert status.tic > status.era assert status.price == Wad(self.sump / Rad(lot)) # cleanup time_travel_by(self.web3, self.flopper.ttl() + 1) assert self.flopper.deal(kick).transact() def test_should_terminate_model_if_auction_expired_due_to_tau(self, kick): # given (model, model_factory) = models(self.keeper, kick) # when self.keeper.check_all_auctions() wait_for_other_threads() # then model_factory.create_model.assert_called_once() model.terminate.assert_not_called() # when time_travel_by(self.web3, self.flopper.tau() + 1) # and self.keeper.check_all_auctions() wait_for_other_threads() # then model_factory.create_model.assert_called_once() model.terminate.assert_called_once() def test_should_terminate_model_if_auction_expired_due_to_ttl_and_somebody_else_won_it( self, kick): # given (model, model_factory) = models(self.keeper, kick) # when self.keeper.check_all_auctions() wait_for_other_threads() # then model_factory.create_model.assert_called_once() model.terminate.assert_not_called() # when self.dent(kick, self.other_address, Wad.from_number(0.000015), self.sump) # and time_travel_by(self.web3, self.flopper.ttl() + 1) # and self.keeper.check_all_auctions() wait_for_other_threads() # then model_factory.create_model.assert_called_once() model.terminate.assert_called_once() # cleanup assert self.flopper.deal(kick).transact() def test_should_terminate_model_if_auction_is_dealt(self, kick): # given (model, model_factory) = models(self.keeper, kick) # when self.keeper.check_all_auctions() wait_for_other_threads() # then model_factory.create_model.assert_called_once() model.terminate.assert_not_called() # when self.dent(kick, self.other_address, Wad.from_number(0.000016), self.sump) # and time_travel_by(self.web3, self.flopper.ttl() + 1) # and self.flopper.deal(kick).transact(from_address=self.other_address) # and self.keeper.check_all_auctions() wait_for_other_threads() # then model_factory.create_model.assert_called_once() model.terminate.assert_called_once() def test_should_not_instantiate_model_if_auction_is_dealt(self, kick): # given (model, model_factory) = models(self.keeper, kick) # and self.dent(kick, self.other_address, Wad.from_number(0.000017), self.sump) # and time_travel_by(self.web3, self.flopper.ttl() + 1) # and assert self.flopper.deal(kick).transact( from_address=self.other_address) # when self.keeper.check_all_auctions() wait_for_other_threads() # then model_factory.create_model.assert_not_called() def test_should_not_do_anything_if_no_output_from_model(self, kick): # given previous_block_number = self.web3.eth.blockNumber # when # [no output from model] # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert self.web3.eth.blockNumber == previous_block_number def test_should_make_initial_bid(self): # given kick = self.flopper.kicks() (model, model_factory) = models(self.keeper, kick) mkr_before = self.mcd.mkr.balance_of(self.keeper_address) # when simulate_model_output(model=model, price=Wad.from_number(575.0)) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then auction = self.flopper.bids(kick) assert round(auction.bid / Rad(auction.lot), 2) == round(Rad.from_number(575.0), 2) mkr_after = self.mcd.mkr.balance_of(self.keeper_address) assert mkr_before == mkr_after # cleanup time_travel_by(self.web3, self.flopper.ttl() + 1) assert self.flopper.deal(kick).transact() def test_should_bid_even_if_there_is_already_a_bidder(self, kick): # given (model, model_factory) = models(self.keeper, kick) mkr_before = self.mcd.mkr.balance_of(self.keeper_address) # and lot = Wad.from_number(0.000016) assert self.flopper.dent( kick, lot, self.sump).transact(from_address=self.other_address) assert self.flopper.bids(kick).lot == lot # when simulate_model_output(model=model, price=Wad.from_number(825.0)) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then auction = self.flopper.bids(kick) assert auction.lot != lot assert round(auction.bid / Rad(auction.lot), 2) == round(Rad.from_number(825.0), 2) mkr_after = self.mcd.mkr.balance_of(self.keeper_address) assert mkr_before == mkr_after # cleanup time_travel_by(self.web3, self.flopper.ttl() + 1) assert self.flopper.deal(kick).transact() def test_should_overbid_itself_if_model_has_updated_the_price(self, kick): # given (model, model_factory) = models(self.keeper, kick) # when simulate_model_output(model=model, price=Wad.from_number(100.0)) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert round(Rad(self.flopper.bids(kick).lot), 2) == round(self.sump / Rad.from_number(100.0), 2) # when simulate_model_output(model=model, price=Wad.from_number(110.0)) self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert self.lot_implies_price(kick, Wad.from_number(110.0)) # cleanup time_travel_by(self.web3, self.flopper.ttl() + 1) assert self.flopper.deal(kick).transact() def test_should_increase_gas_price_of_pending_transactions_if_model_increases_gas_price( self, kick): # given (model, model_factory) = models(self.keeper, kick) # when simulate_model_output(model=model, price=Wad.from_number(120.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(120.0), gas_price=15) # and self.keeper.check_for_bids() wait_for_other_threads() # then assert self.lot_implies_price(kick, Wad.from_number(120.0)) assert self.web3.eth.getBlock( 'latest', full_transactions=True).transactions[0].gasPrice == 15 # cleanup time_travel_by(self.web3, self.flopper.ttl() + 1) assert self.flopper.deal(kick).transact() def test_should_replace_pending_transactions_if_model_raises_bid_and_increases_gas_price( self, kick): # given (model, model_factory) = models(self.keeper, kick) # when simulate_model_output(model=model, price=Wad.from_number(50.0), gas_price=10) # and self.start_ignoring_transactions() # and self.keeper.check_all_auctions() self.keeper.check_for_bids() # and time.sleep(2) # and self.end_ignoring_transactions() # and simulate_model_output(model=model, price=Wad.from_number(60.0), gas_price=15) # and self.keeper.check_for_bids() wait_for_other_threads() # then assert self.lot_implies_price(kick, Wad.from_number(60.0)) assert self.web3.eth.getBlock( 'latest', full_transactions=True).transactions[0].gasPrice == 15 # cleanup time_travel_by(self.web3, self.flopper.ttl() + 1) assert self.flopper.deal(kick).transact() def test_should_replace_pending_transactions_if_model_lowers_bid_and_increases_gas_price( self, kick): # given (model, model_factory) = models(self.keeper, kick) # when simulate_model_output(model=model, price=Wad.from_number(80.0), gas_price=10) # and self.start_ignoring_transactions() # and self.keeper.check_all_auctions() # and time.sleep(2) # and self.end_ignoring_transactions() # and simulate_model_output(model=model, price=Wad.from_number(70.0), gas_price=15) # and self.keeper.check_for_bids() wait_for_other_threads() # then assert self.lot_implies_price(kick, Wad.from_number(70.0)) assert self.web3.eth.getBlock( 'latest', full_transactions=True).transactions[0].gasPrice == 15 # cleanup time_travel_by(self.web3, self.flopper.ttl() + 1) assert self.flopper.deal(kick).transact() def test_should_not_bid_on_rounding_errors_with_small_amounts(self, kick): # given (model, model_factory) = models(self.keeper, kick) # when simulate_model_output(model=model, price=Wad.from_number(1400.0)) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert self.flopper.bids(kick).lot == Wad(self.sump / Rad.from_number(1400.0)) # when tx_count = self.web3.eth.getTransactionCount( self.keeper_address.address) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert self.web3.eth.getTransactionCount( self.keeper_address.address) == tx_count def test_should_deal_when_we_won_the_auction(self, kick): # given (model, model_factory) = models(self.keeper, kick) # when simulate_model_output(model=model, price=Wad.from_number(825.0)) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert self.lot_implies_price(kick, Wad.from_number(825.0)) mkr_before = self.mcd.mkr.balance_of(self.keeper_address) # when time_travel_by(self.web3, self.flopper.ttl() + 1) # and self.keeper.check_all_auctions() wait_for_other_threads() # then mkr_after = self.mcd.mkr.balance_of(self.keeper_address) assert mkr_before < mkr_after def test_should_not_deal_when_auction_finished_but_somebody_else_won( self, kick): # given mkr_before = self.mcd.mkr.balance_of(self.keeper_address) # and self.dent(kick, self.other_address, Wad.from_number(0.000015), self.sump) assert self.flopper.bids(kick).lot == Wad.from_number(0.000015) # when time_travel_by(self.web3, self.flopper.ttl() + 1) # and self.keeper.check_all_auctions() wait_for_other_threads() # then mkr_after = self.mcd.mkr.balance_of(self.keeper_address) assert mkr_before == mkr_after def test_should_obey_gas_price_provided_by_the_model(self, kick): # given (model, model_factory) = models(self.keeper, kick) # when simulate_model_output(model=model, price=Wad.from_number(800.0), gas_price=175000) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert self.flopper.bids(kick).guy == self.keeper_address assert self.web3.eth.getBlock( 'latest', full_transactions=True).transactions[0].gasPrice == 175000 # cleanup time_travel_by(self.web3, self.flopper.ttl() + 1) assert self.flopper.deal(kick).transact() def test_should_use_default_gas_price_if_not_provided_by_the_model( self, kick): # given (model, model_factory) = models(self.keeper, kick) # when simulate_model_output(model=model, price=Wad.from_number(850.0)) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert self.flopper.bids(kick).guy == self.keeper_address assert self.web3.eth.getBlock('latest', full_transactions=True).transactions[0].gasPrice == \ self.default_gas_price # cleanup time_travel_by(self.web3, self.flopper.ttl() + 1) assert self.flopper.deal(kick).transact() def test_should_change_gas_strategy_when_model_output_changes(self, kick): # given (model, model_factory) = models(self.keeper, kick) # when first_bid = Wad.from_number(90) simulate_model_output(model=model, price=first_bid, gas_price=2000) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert self.web3.eth.getBlock( 'latest', full_transactions=True).transactions[0].gasPrice == 2000 # when second_bid = Wad.from_number(100) simulate_model_output(model=model, price=second_bid) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert round(Rad(self.flopper.bids(kick).lot), 2) == round(self.sump / Rad(second_bid), 2) assert self.web3.eth.getBlock('latest', full_transactions=True).transactions[0].gasPrice == \ self.default_gas_price # when third_bid = Wad.from_number(110) new_gas_price = int(self.default_gas_price * 1.25) simulate_model_output(model=model, price=third_bid, gas_price=new_gas_price) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert round(Rad(self.flopper.bids(kick).lot), 2) == round(self.sump / Rad(third_bid), 2) assert self.web3.eth.getBlock( 'latest', full_transactions=True).transactions[0].gasPrice == new_gas_price # cleanup time_travel_by(self.web3, self.flopper.ttl() + 1) assert self.flopper.deal(kick).transact() @classmethod def teardown_class(cls): cls.cleanup_debt(web3(), mcd(web3()), other_address(web3())) @classmethod def cleanup_debt(cls, web3, mcd, address): # Cancel out surplus and debt dai_vow = mcd.vat.dai(mcd.vow.address) assert dai_vow <= mcd.vow.woe() assert mcd.vow.heal(dai_vow).transact()
class TestAuctionKeeperFlapper(TransactionIgnoringTest): 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.flapper = self.mcd.flapper self.flapper.approve(self.mcd.mkr.address, directly(from_address=self.other_address)) self.keeper = AuctionKeeper(args=args(f"--eth-from {self.keeper_address} " f"--type flap " f"--from-block 1 " f"--model ./bogus-model.sh"), web3=self.web3) self.keeper.approve() mint_mkr(self.mcd.mkr, self.keeper_address, Wad.from_number(50000)) mint_mkr(self.mcd.mkr, self.other_address, Wad.from_number(50000)) assert isinstance(self.keeper.gas_price, DynamicGasPrice) # Since no args were assigned, gas strategy should return a GeometricGasPrice starting at the node gas price self.default_gas_price = get_node_gas_price(self.web3) def test_should_detect_flap(self, web3, mcd, c, gal_address, keeper_address): # given some MKR is available to the keeper and a count of flap auctions mint_mkr(mcd.mkr, keeper_address, Wad.from_number(50000)) kicks = mcd.flapper.kicks() # when surplus is generated create_cdp_with_surplus(mcd, c, gal_address) self.keeper.check_flap() wait_for_other_threads() # then ensure another flap auction was kicked off kick = mcd.flapper.kicks() assert kick == kicks + 1 # clean up by letting someone else bid and waiting until the auction ends auction = self.flapper.bids(kick) assert self.flapper.tend(kick, auction.lot, Wad.from_number(30)).transact(from_address=self.other_address) time_travel_by(web3, mcd.flapper.ttl() + 1) def test_should_start_a_new_model_and_provide_it_with_info_on_auction_kick(self, kick): # given (model, model_factory) = models(self.keeper, kick) # when self.keeper.check_all_auctions() wait_for_other_threads() # then model_factory.create_model.assert_called_once_with(Parameters(flipper=None, flapper=self.flapper.address, flopper=None, id=kick)) # and status = model.send_status.call_args[0][0] assert status.id == kick assert status.flipper is None assert status.flapper == self.flapper.address assert status.flopper is None assert status.bid == Wad(0) assert status.lot == self.mcd.vow.bump() assert status.tab is None assert status.beg == Wad.from_number(1.05) assert status.guy == self.mcd.vow.address assert status.era > 0 assert status.end < status.era + self.flapper.tau() + 1 assert status.tic == 0 assert status.price is None def test_should_provide_model_with_updated_info_after_our_own_bid(self): # given kick = self.flapper.kicks() (model, model_factory) = models(self.keeper, kick) # when self.keeper.check_all_auctions() wait_for_other_threads() # then assert model.send_status.call_count == 1 # when simulate_model_output(model=model, price=Wad.from_number(9)) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # and self.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 is None assert status.flapper == self.flapper.address assert status.flopper is None assert status.bid == Wad(self.flapper.bids(kick).lot / Rad.from_number(9)) assert status.lot == self.mcd.vow.bump() assert status.tab is None assert status.beg == Wad.from_number(1.05) assert status.guy == self.keeper_address assert status.era > 0 assert status.end > status.era assert status.tic > status.era assert round(status.price, 2) == round(Wad.from_number(9), 2) # cleanup time_travel_by(self.web3, self.flapper.ttl() + 1) assert self.flapper.deal(kick).transact() def test_should_provide_model_with_updated_info_after_somebody_else_bids(self, kick): # given (model, model_factory) = models(self.keeper, kick) # when self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert model.send_status.call_count == 1 # when auction = self.flapper.bids(kick) assert Wad.from_number(40) > auction.bid assert self.flapper.tend(kick, auction.lot, Wad.from_number(40)).transact(from_address=self.other_address) # and self.keeper.check_all_auctions() wait_for_other_threads() auction = self.flapper.bids(kick) # then assert model.send_status.call_count > 1 # and status = model.send_status.call_args[0][0] assert status.id == kick assert status.flipper is None assert status.flapper == self.flapper.address assert status.flopper is None assert status.bid == Wad.from_number(40) assert status.lot == auction.lot assert status.tab is None assert status.beg == Wad.from_number(1.05) assert status.guy == self.other_address assert status.era > 0 assert status.end > status.era assert status.tic > status.era assert status.price == Wad(auction.lot / Rad(auction.bid)) # cleanup time_travel_by(self.web3, self.flapper.ttl() + 1) assert self.flapper.deal(kick).transact() def test_should_tick_if_auction_expired_due_to_tau(self, kick): # given (model, model_factory) = models(self.keeper, kick) # when self.keeper.check_all_auctions() wait_for_other_threads() # then model_factory.create_model.assert_called_once() model.terminate.assert_not_called() # when time_travel_by(self.web3, self.flapper.tau() + 1) # and simulate_model_output(model=model, price=Wad.from_number(9.0)) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then model.terminate.assert_not_called() auction = self.flapper.bids(kick) assert round(Wad(auction.lot) / auction.bid, 2) == round(Wad.from_number(9.0), 2) # cleanup time_travel_by(self.web3, self.flapper.ttl() + 1) model_factory.create_model.assert_called_once() self.keeper.check_all_auctions() model.terminate.assert_called_once() def test_should_terminate_model_if_auction_expired_due_to_ttl_and_somebody_else_won_it(self, kick): # given (model, model_factory) = models(self.keeper, kick) # when self.keeper.check_all_auctions() wait_for_other_threads() # then model_factory.create_model.assert_called_once() model.terminate.assert_not_called() # when auction = self.flapper.bids(kick) assert self.flapper.tend(kick, auction.lot, Wad.from_number(40)).transact(from_address=self.other_address) # and time_travel_by(self.web3, self.flapper.ttl() + 1) # and self.keeper.check_all_auctions() wait_for_other_threads() # then model_factory.create_model.assert_called_once() model.terminate.assert_called_once() def test_should_terminate_model_if_auction_is_dealt(self, kick): # given kick = self.flapper.kicks() (model, model_factory) = models(self.keeper, kick) # when self.keeper.check_all_auctions() wait_for_other_threads() # then model_factory.create_model.assert_called_once() model.terminate.assert_not_called() # when auction = self.flapper.bids(kick) assert self.flapper.tend(kick, auction.lot, Wad.from_number(40)).transact(from_address=self.other_address) # and time_travel_by(self.web3, self.flapper.ttl() + 1) # and assert self.flapper.deal(kick).transact(from_address=self.other_address) # and self.keeper.check_all_auctions() wait_for_other_threads() # then model_factory.create_model.assert_called_once() model.terminate.assert_called_once() def test_should_not_instantiate_model_if_auction_is_dealt(self, kick): # given (model, model_factory) = models(self.keeper, kick) # and auction = self.flapper.bids(kick) self.flapper.tend(kick, auction.lot, Wad.from_number(40)).transact(from_address=self.other_address) # and time_travel_by(self.web3, self.flapper.ttl() + 1) # and self.flapper.deal(kick).transact(from_address=self.other_address) # when self.keeper.check_all_auctions() wait_for_other_threads() # then model_factory.create_model.assert_not_called() def test_should_not_do_anything_if_no_output_from_model(self, kick): # given previous_block_number = self.web3.eth.blockNumber # when # [no output from model] # and self.keeper.check_all_auctions() wait_for_other_threads() # then assert self.web3.eth.blockNumber == previous_block_number def test_should_make_initial_bid(self): # given kick = self.flapper.kicks() (model, model_factory) = models(self.keeper, kick) # when simulate_model_output(model=model, price=Wad.from_number(10.0)) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then auction = self.flapper.bids(kick) assert round(Wad(auction.lot) / auction.bid, 2) == round(Wad.from_number(10.0), 2) # cleanup time_travel_by(self.web3, self.flapper.ttl() + 1) assert self.flapper.deal(kick).transact() def test_should_bid_even_if_there_is_already_a_bidder(self, kick): # given (model, model_factory) = models(self.keeper, kick) # and auction = self.flapper.bids(kick) assert self.flapper.tend(kick, auction.lot, Wad.from_number(16)).transact(from_address=self.other_address) assert self.flapper.bids(kick).bid == Wad.from_number(16) # when simulate_model_output(model=model, price=Wad.from_number(0.0000005)) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then auction = self.flapper.bids(kick) assert round(Wad(auction.lot) / auction.bid, 2) == round(Wad.from_number(0.0000005), 2) # cleanup time_travel_by(self.web3, self.flapper.ttl() + 1) assert self.flapper.deal(kick).transact() def test_should_overbid_itself_if_model_has_updated_the_price(self, kick): # given (model, model_factory) = models(self.keeper, kick) lot = self.flapper.bids(kick).lot # when first_bid = Wad.from_number(0.0000004) simulate_model_output(model=model, price=first_bid) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert self.flapper.bids(kick).bid == Wad(lot / Rad(first_bid)) # when second_bid = Wad.from_number(0.0000003) simulate_model_output(model=model, price=second_bid) self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert self.flapper.bids(kick).bid == Wad(lot / Rad(second_bid)) # cleanup time_travel_by(self.web3, self.flapper.ttl() + 1) assert self.flapper.deal(kick).transact() def test_should_increase_gas_price_of_pending_transactions_if_model_increases_gas_price(self, kick): # given (model, model_factory) = models(self.keeper, kick) lot = self.flapper.bids(kick).lot # when simulate_model_output(model=model, price=Wad.from_number(10.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(10.0), gas_price=15) # and self.keeper.check_for_bids() wait_for_other_threads() # then assert self.flapper.bids(kick).bid == Wad(lot) / Wad.from_number(10.0) assert self.web3.eth.getBlock('latest', full_transactions=True).transactions[0].gasPrice == 15 # cleanup time_travel_by(self.web3, self.flapper.ttl() + 1) assert self.flapper.deal(kick).transact() def test_should_replace_pending_transactions_if_model_raises_bid_and_increases_gas_price(self, kick): # given (model, model_factory) = models(self.keeper, kick) lot = self.flapper.bids(kick).lot # when simulate_model_output(model=model, price=Wad.from_number(9.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(8.0), gas_price=15) # and self.keeper.check_for_bids() wait_for_other_threads() # then assert round(self.flapper.bids(kick).bid, 2) == round(Wad(lot / Rad.from_number(8.0)), 2) assert self.web3.eth.getBlock('latest', full_transactions=True).transactions[0].gasPrice == 15 # cleanup time_travel_by(self.web3, self.flapper.ttl() + 1) assert self.flapper.deal(kick).transact() def test_should_replace_pending_transactions_if_model_lowers_bid_and_increases_gas_price(self, kick): # given (model, model_factory) = models(self.keeper, kick) lot = self.flapper.bids(kick).lot # when simulate_model_output(model=model, price=Wad.from_number(10.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(8.0), gas_price=15) # and self.keeper.check_for_bids() wait_for_other_threads() # then assert self.flapper.bids(kick).bid == Wad(lot) / Wad.from_number(8.0) assert self.web3.eth.getBlock('latest', full_transactions=True).transactions[0].gasPrice == 15 # cleanup time_travel_by(self.web3, self.flapper.ttl() + 1) assert self.flapper.deal(kick).transact() def test_should_not_bid_on_rounding_errors_with_small_amounts(self, kick): # given (model, model_factory) = models(self.keeper, kick) lot = self.flapper.bids(kick).lot # when price = Wad.from_number(9.0)-Wad(5) simulate_model_output(model=model, price=price) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert self.flapper.bids(kick).bid == Wad(lot) / Wad(price) # when tx_count = self.web3.eth.getTransactionCount(self.keeper_address.address) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert self.web3.eth.getTransactionCount(self.keeper_address.address) == tx_count # cleanup time_travel_by(self.web3, self.flapper.ttl() + 1) assert self.flapper.deal(kick).transact() def test_should_deal_when_we_won_the_auction(self, kick): # given (model, model_factory) = models(self.keeper, kick) # when simulate_model_output(model=model, price=Wad.from_number(8.0)) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then auction = self.flapper.bids(kick) assert auction.bid > Wad(0) assert round(Wad(auction.lot) / auction.bid, 2) == round(Wad.from_number(8.0), 2) dai_before = self.mcd.vat.dai(self.keeper_address) # when time_travel_by(self.web3, self.flapper.ttl() + 1) # and self.keeper.check_all_auctions() wait_for_other_threads() dai_after = self.mcd.vat.dai(self.keeper_address) # then assert dai_before < dai_after def test_should_not_deal_when_auction_finished_but_somebody_else_won(self, kick): # given (model, model_factory) = models(self.keeper, kick) lot = self.flapper.bids(kick).lot # and assert self.flapper.tend(kick, lot, Wad.from_number(16)).transact(from_address=self.other_address) assert self.flapper.bids(kick).bid == Wad.from_number(16) # when time_travel_by(self.web3, self.flapper.ttl() + 1) # and self.keeper.check_all_auctions() wait_for_other_threads() # then assert self.flapper.bids(kick).bid == Wad.from_number(16) def test_should_obey_gas_price_provided_by_the_model(self, kick): # given (model, model_factory) = models(self.keeper, kick) # when simulate_model_output(model=model, price=Wad.from_number(10.0), gas_price=175000) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert self.web3.eth.getBlock('latest', full_transactions=True).transactions[0].gasPrice == 175000 # cleanup time_travel_by(self.web3, self.flapper.ttl() + 1) assert self.flapper.deal(kick).transact() def test_should_use_default_gas_price_if_not_provided_by_the_model(self, kick): # given (model, model_factory) = models(self.keeper, kick) # when simulate_model_output(model=model, price=Wad.from_number(10.0)) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then auction = self.flapper.bids(kick) assert auction.guy == self.keeper_address assert auction.bid > Wad(0) assert self.web3.eth.getBlock('latest', full_transactions=True).transactions[0].gasPrice == \ self.default_gas_price print(f"tx gas price is {self.web3.eth.getBlock('latest', full_transactions=True).transactions[0].gasPrice}, web3.eth.gasPrice is {self.web3.eth.gasPrice}") # cleanup time_travel_by(self.web3, self.flapper.ttl() + 1) assert self.flapper.deal(kick).transact() def test_should_change_gas_strategy_when_model_output_changes(self, kick): # given (model, model_factory) = models(self.keeper, kick) lot = self.flapper.bids(kick).lot # when first_bid = Wad.from_number(0.0000009) simulate_model_output(model=model, price=first_bid, gas_price=2000) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert self.web3.eth.getBlock('latest', full_transactions=True).transactions[0].gasPrice == 2000 # when second_bid = Wad.from_number(0.0000006) simulate_model_output(model=model, price=second_bid) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert self.flapper.bids(kick).bid == Wad(lot / Rad(second_bid)) assert self.web3.eth.getBlock('latest', full_transactions=True).transactions[0].gasPrice == \ self.default_gas_price # when third_bid = Wad.from_number(0.0000003) new_gas_price = int(self.default_gas_price*1.25) simulate_model_output(model=model, price=third_bid, gas_price=new_gas_price) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert self.flapper.bids(kick).bid == Wad(lot / Rad(third_bid)) assert self.web3.eth.getBlock('latest', full_transactions=True).transactions[0].gasPrice == new_gas_price # cleanup time_travel_by(self.web3, self.flapper.ttl() + 1) assert self.flapper.deal(kick).transact() @classmethod def teardown_class(cls): cls.mcd = mcd(web3()) cls.liquidate_urn(web3(), cls.mcd, c(cls.mcd), gal_address(web3()), our_address(web3())) cls.repay_vault(web3(), cls.mcd, c(cls.mcd), gal_address(web3())) @classmethod 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) @classmethod def repay_vault(cls, web3, mcd, c, gal_address): # Borrow dai from ETH-C to repay the ETH-B vault urn = mcd.vat.urn(c.ilk, gal_address) # Procure enough Dai to close the vault dai_balance: Wad = mcd.dai.balance_of(gal_address) vat_balance: Wad = Wad(mcd.vat.dai(gal_address)) if vat_balance < urn.art: needed_in_vat = urn.art - vat_balance if dai_balance < needed_in_vat: print("Purchasing Dai to repay vault") purchase_dai(needed_in_vat - dai_balance, gal_address) print("Joining Dai to repay vault") mcd.dai_adapter.join(needed_in_vat).transact(from_address=gal_address) print(f"We have {mcd.vat.dai(gal_address)} Dai to pay off {urn.art} debt") print("Closing vault") mcd.vat.validate_frob(c.ilk, urn.address, Wad(0), urn.art*-1) assert mcd.vat.frob(c.ilk, urn.address, Wad(0), urn.art*-1).transact(from_address=gal_address) mcd.vat.validate_frob(c.ilk, urn.address, urn.ink * -1, Wad(0)) assert mcd.vat.frob(c.ilk, urn.address, urn.ink * -1, Wad(0)).transact(from_address=gal_address) urn = mcd.vat.urn(c.ilk, gal_address) assert urn.ink == Wad(0) assert urn.art == Wad(0)
class TestAuctionKeeperDebtAuction(TransactionIgnoringTest): def setup_method(self): self.web3 = get_web3() self.our_address = get_our_address(self.web3) self.keeper_address = get_keeper_address(self.web3) self.other_address = get_other_address(self.web3) self.auction_income_recipient_address = get_auction_income_recipient_address( self.web3) self.geb = get_geb(self.web3) self.debt_auction_house = self.geb.debt_auction_house self.debt_auction_house.approve( self.geb.safe_engine.address, approval_function=approve_safe_modification_directly( from_address=self.keeper_address)) self.debt_auction_house.approve( self.geb.safe_engine.address, approval_function=approve_safe_modification_directly( from_address=self.other_address)) self.keeper = AuctionKeeper(args=args( f"--eth-from {self.keeper_address} " f"--type debt " 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_system_coin(self.geb, self.geb.collaterals['ETH-C'], self.keeper_address, Wad.from_number(200.00000)) reserve_system_coin(self.geb, self.geb.collaterals['ETH-C'], self.other_address, Wad.from_number(200.00000)) self.debt_auction_bid_size = self.geb.accounting_engine.debt_auction_bid_size( ) # Rad def decrease_sold_amount(self, id: int, address: Address, amount_to_sell: Wad, bid_amount: Rad): assert (isinstance(id, int)) assert (isinstance(amount_to_sell, Wad)) assert (isinstance(bid_amount, Rad)) assert self.debt_auction_house.contract_enabled() == 1 current_bid = self.debt_auction_house.bids(id) assert current_bid.high_bidder != Address( "0x0000000000000000000000000000000000000000") assert current_bid.bid_expiry > datetime.now().timestamp( ) or current_bid.bid_expiry == 0 assert current_bid.auction_deadline > datetime.now().timestamp() assert bid_amount == current_bid.bid_amount assert Wad(0) < amount_to_sell < current_bid.amount_to_sell assert self.debt_auction_house.bid_decrease( ) * amount_to_sell <= current_bid.amount_to_sell assert self.debt_auction_house.decrease_sold_amount( id, amount_to_sell, bid_amount).transact(from_address=address) def amount_to_sell_implies_price(self, auction_id: int, price: Wad) -> bool: return round( Rad(self.debt_auction_house.bids(auction_id).amount_to_sell), 2) == round(self.debt_auction_bid_size / Rad(price), 2) def test_should_detect_debt_auction(self, web3, c, geb, other_address, keeper_address): # given a count of debt auctions reserve_system_coin(geb, c, keeper_address, Wad.from_number(230)) auctions_started = geb.debt_auction_house.auctions_started() # and an undercollateralized SAFE is liquidated critical_safe = create_critical_safe(geb, c, Wad.from_number(1), other_address, draw_system_coin=False) assert geb.liquidation_engine.liquidate_safe( critical_safe.collateral_type, critical_safe).transact() # when the auction ends without debt being covered if isinstance(c.collateral_auction_house, EnglishCollateralAuctionHouse): time_travel_by( web3, c.collateral_auction_house.total_auction_length() + 1) # then ensure testchain is in the appropriate state total_surplus = geb.safe_engine.coin_balance( geb.accounting_engine.address) total_debt = geb.safe_engine.debt_balance( geb.accounting_engine.address) unqueued_unauctioned_debt = ( geb.safe_engine.debt_balance(geb.accounting_engine.address) - geb.accounting_engine.total_queued_debt() ) - geb.accounting_engine.total_on_auction_debt() debt_queue = geb.accounting_engine.total_queued_debt() debt_auction_bid_size = geb.accounting_engine.debt_auction_bid_size() wait = geb.accounting_engine.pop_debt_delay() assert total_surplus < total_debt assert unqueued_unauctioned_debt + debt_queue >= debt_auction_bid_size assert wait == 0 # when self.keeper.check_debt() wait_for_other_threads() # then ensure another debt auction was started auction_id = geb.debt_auction_house.auctions_started() assert auction_id == auctions_started + 1 # clean up by letting someone else bid and waiting until the auction ends self.decrease_sold_amount(auction_id, self.other_address, Wad.from_number(0.000012), self.debt_auction_bid_size) time_travel_by(web3, geb.debt_auction_house.bid_duration() + 1) def test_should_start_a_new_model_and_provide_it_with_info_on_auction_start( self, auction_id): # given (model, model_factory) = models(self.keeper, auction_id) # when self.keeper.check_all_auctions() wait_for_other_threads() # then model_factory.create_model.assert_called_once_with( Parameters(collateral_auction_house=None, surplus_auction_house=None, debt_auction_house=self.debt_auction_house.address, staked_token_auction_house=None, id=auction_id)) # and status = model.send_status.call_args[0][0] assert status.id == auction_id assert status.collateral_auction_house is None assert status.surplus_auction_house is None assert status.debt_auction_house == self.debt_auction_house.address assert status.bid_amount > Rad.from_number(0) assert status.amount_to_sell == self.geb.accounting_engine.initial_debt_auction_minted_tokens( ) assert status.amount_to_raise is None assert status.bid_decrease > Wad.from_number(1) assert status.high_bidder == self.geb.accounting_engine.address assert status.block_time > 0 assert status.auction_deadline < status.block_time + self.debt_auction_house.total_auction_length( ) + 1 assert status.bid_expiry == 0 assert status.price == Wad(status.bid_amount * self.geb.oracle_relayer.redemption_price() / Rad(status.amount_to_sell)) def test_should_provide_model_with_updated_info_after_our_own_bid(self): # given auction_id = self.debt_auction_house.auctions_started() (model, model_factory) = models(self.keeper, auction_id) # when self.keeper.check_all_auctions() wait_for_other_threads() # then assert model.send_status.call_count == 1 # when price = Wad.from_number(250.0) simulate_model_output(model=model, price=price) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert model.send_status.call_count > 1 last_bid = self.debt_auction_house.bids(auction_id) # and status = model.send_status.call_args[0][0] assert status.id == auction_id assert status.collateral_auction_house is None assert status.surplus_auction_house is None assert status.debt_auction_house == self.debt_auction_house.address assert status.bid_amount == last_bid.bid_amount assert status.amount_to_sell == Wad( last_bid.bid_amount * self.geb.oracle_relayer.redemption_price() / Rad(price)) assert status.amount_to_raise is None assert status.bid_decrease > Wad.from_number(1) assert status.high_bidder == self.keeper_address assert status.block_time > 0 assert status.auction_deadline > status.block_time assert status.bid_expiry > status.block_time assert status.price == price # cleanup time_travel_by(self.web3, self.debt_auction_house.bid_duration() + 1) assert self.debt_auction_house.settle_auction(auction_id).transact() def test_should_provide_model_with_updated_info_after_somebody_else_bids( self, auction_id): # given (model, model_factory) = models(self.keeper, auction_id) # when self.keeper.check_all_auctions() wait_for_other_threads() # then assert model.send_status.call_count == 1 # when amount_to_sell = Wad.from_number(0.0000001) assert self.debt_auction_house.decrease_sold_amount( auction_id, amount_to_sell, self.debt_auction_bid_size).transact( from_address=self.other_address) # and self.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 == auction_id assert status.collateral_auction_house is None assert status.surplus_auction_house is None assert status.debt_auction_house == self.debt_auction_house.address assert status.bid_amount == self.debt_auction_bid_size assert status.amount_to_sell == amount_to_sell assert status.amount_to_raise is None assert status.bid_decrease > Wad.from_number(1) assert status.high_bidder == self.other_address assert status.block_time > 0 assert status.auction_deadline > status.block_time assert status.bid_expiry > status.block_time assert status.price == Wad(self.debt_auction_bid_size * self.geb.oracle_relayer.redemption_price() / Rad(amount_to_sell)) # cleanup time_travel_by(self.web3, self.debt_auction_house.bid_duration() + 1) assert self.debt_auction_house.settle_auction(auction_id).transact() def test_should_restart_auction_if_auction_expired_due_to_total_auction_length( self, auction_id): # given (model, model_factory) = models(self.keeper, auction_id) # when self.keeper.check_all_auctions() wait_for_other_threads() # then model_factory.create_model.assert_called_once() model.terminate.assert_not_called() # when time_travel_by(self.web3, self.debt_auction_house.total_auction_length() + 1) # and simulate_model_output(model=model, price=Wad.from_number(555.0)) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then model.terminate.assert_not_called() auction = self.debt_auction_house.bids(auction_id) assert round( auction.bid_amount * self.geb.oracle_relayer.redemption_price() / Rad(auction.amount_to_sell), 2) == round(Rad.from_number(555.0), 2) # cleanup time_travel_by(self.web3, self.debt_auction_house.bid_duration() + 1) model_factory.create_model.assert_called_once() self.keeper.check_all_auctions() model.terminate.assert_called_once() def test_should_terminate_model_if_auction_expired_due_to_bid_duration_and_somebody_else_won_it( self, auction_id): # given (model, model_factory) = models(self.keeper, auction_id) # when self.keeper.check_all_auctions() wait_for_other_threads() # then model_factory.create_model.assert_called_once() model.terminate.assert_not_called() # when self.decrease_sold_amount(auction_id, self.other_address, Wad.from_number(0.000015), self.debt_auction_bid_size) # and time_travel_by(self.web3, self.debt_auction_house.bid_duration() + 1) # and self.keeper.check_all_auctions() wait_for_other_threads() # then model_factory.create_model.assert_called_once() model.terminate.assert_called_once() # cleanup assert self.debt_auction_house.settle_auction(auction_id).transact() def test_should_terminate_model_if_auction_is_settled(self, auction_id): # given (model, model_factory) = models(self.keeper, auction_id) # when self.keeper.check_all_auctions() wait_for_other_threads() # then model_factory.create_model.assert_called_once() model.terminate.assert_not_called() # when self.decrease_sold_amount(auction_id, self.other_address, Wad.from_number(0.000016), self.debt_auction_bid_size) # and time_travel_by(self.web3, self.debt_auction_house.bid_duration() + 1) # and self.debt_auction_house.settle_auction(auction_id).transact( from_address=self.other_address) # and self.keeper.check_all_auctions() wait_for_other_threads() # then model_factory.create_model.assert_called_once() model.terminate.assert_called_once() def test_should_not_instantiate_model_if_auction_is_settled( self, auction_id): # given (model, model_factory) = models(self.keeper, auction_id) # and self.decrease_sold_amount(auction_id, self.other_address, Wad.from_number(0.000017), self.debt_auction_bid_size) # and time_travel_by(self.web3, self.debt_auction_house.bid_duration() + 1) # and assert self.debt_auction_house.settle_auction(auction_id).transact( from_address=self.other_address) # when self.keeper.check_all_auctions() wait_for_other_threads() # then model_factory.create_model.assert_not_called() def test_should_not_do_anything_if_no_output_from_model(self, auction_id): # given previous_block_number = self.web3.eth.blockNumber # when # [no output from model] # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert self.web3.eth.blockNumber == previous_block_number def test_should_make_initial_bid(self): # given auction_id = self.debt_auction_house.auctions_started() (model, model_factory) = models(self.keeper, auction_id) prot_before = self.geb.prot.balance_of(self.keeper_address) # when simulate_model_output(model=model, price=Wad.from_number(575.0)) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then auction = self.debt_auction_house.bids(auction_id) assert round( auction.bid_amount * self.geb.oracle_relayer.redemption_price() / Rad(auction.amount_to_sell), 2) == round(Rad.from_number(575.0), 2) prot_after = self.geb.prot.balance_of(self.keeper_address) assert prot_before == prot_after # cleanup time_travel_by(self.web3, self.debt_auction_house.bid_duration() + 1) assert self.debt_auction_house.settle_auction(auction_id).transact() def test_should_bid_even_if_there_is_already_a_bidder(self, auction_id): # given (model, model_factory) = models(self.keeper, auction_id) prot_before = self.geb.prot.balance_of(self.keeper_address) # and amount_to_sell = Wad.from_number(0.000016) assert self.debt_auction_house.decrease_sold_amount( auction_id, amount_to_sell, self.debt_auction_bid_size).transact( from_address=self.other_address) assert self.debt_auction_house.bids( auction_id).amount_to_sell == amount_to_sell # when simulate_model_output(model=model, price=Wad.from_number(825.0)) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then auction = self.debt_auction_house.bids(auction_id) assert auction.amount_to_sell != amount_to_sell assert round( auction.bid_amount * self.geb.oracle_relayer.redemption_price() / Rad(auction.amount_to_sell), 2) == round(Rad.from_number(825.0), 2) prot_after = self.geb.prot.balance_of(self.keeper_address) assert prot_before == prot_after # cleanup time_travel_by(self.web3, self.debt_auction_house.bid_duration() + 1) assert self.debt_auction_house.settle_auction(auction_id).transact() def test_should_overbid_itself_if_model_has_updated_the_price( self, auction_id): # given (model, model_factory) = models(self.keeper, auction_id) # when simulate_model_output(model=model, price=Wad.from_number(100.0)) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert round( Rad(self.debt_auction_house.bids(auction_id).amount_to_sell), 2) == round(self.debt_auction_bid_size / Rad.from_number(100.0), 2) # when simulate_model_output(model=model, price=Wad.from_number(110.0)) self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert self.amount_to_sell_implies_price(auction_id, Wad.from_number(110.0)) # cleanup time_travel_by(self.web3, self.debt_auction_house.bid_duration() + 1) assert self.debt_auction_house.settle_auction(auction_id).transact() def test_should_increase_gas_price_of_pending_transactions_if_model_increases_gas_price( self, auction_id): # given (model, model_factory) = models(self.keeper, auction_id) # when simulate_model_output(model=model, price=Wad.from_number(120.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(120.0), gas_price=15) # and self.keeper.check_for_bids() wait_for_other_threads() # then assert self.amount_to_sell_implies_price(auction_id, Wad.from_number(120.0)) assert self.web3.eth.getBlock( 'latest', full_transactions=True).transactions[0].gasPrice == 15 # cleanup time_travel_by(self.web3, self.debt_auction_house.bid_duration() + 1) assert self.debt_auction_house.settle_auction(auction_id).transact() def test_should_replace_pending_transactions_if_model_raises_bid_and_increases_gas_price( self, auction_id): # given (model, model_factory) = models(self.keeper, auction_id) # when simulate_model_output(model=model, price=Wad.from_number(50.0), gas_price=10) # and self.start_ignoring_transactions() # and self.keeper.check_all_auctions() self.keeper.check_for_bids() # and time.sleep(2) # and self.end_ignoring_transactions() # and simulate_model_output(model=model, price=Wad.from_number(60.0), gas_price=15) # and self.keeper.check_for_bids() wait_for_other_threads() # then assert self.amount_to_sell_implies_price(auction_id, Wad.from_number(60.0)) assert self.web3.eth.getBlock( 'latest', full_transactions=True).transactions[0].gasPrice == 15 # cleanup time_travel_by(self.web3, self.debt_auction_house.bid_duration() + 1) assert self.debt_auction_house.settle_auction(auction_id).transact() def test_should_replace_pending_transactions_if_model_lowers_bid_and_increases_gas_price( self, auction_id): # given (model, model_factory) = models(self.keeper, auction_id) # when simulate_model_output(model=model, price=Wad.from_number(80.0), gas_price=10) # and self.start_ignoring_transactions() # and self.keeper.check_all_auctions() # and time.sleep(2) # and self.end_ignoring_transactions() # and simulate_model_output(model=model, price=Wad.from_number(70.0), gas_price=15) # and self.keeper.check_for_bids() wait_for_other_threads() # then assert self.amount_to_sell_implies_price(auction_id, Wad.from_number(70.0)) assert self.web3.eth.getBlock( 'latest', full_transactions=True).transactions[0].gasPrice == 15 # cleanup time_travel_by(self.web3, self.debt_auction_house.bid_duration() + 1) assert self.debt_auction_house.settle_auction(auction_id).transact() def test_should_not_bid_on_rounding_errors_with_small_amounts( self, auction_id): # given (model, model_factory) = models(self.keeper, auction_id) # when simulate_model_output(model=model, price=Wad.from_number(1400.0)) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert self.debt_auction_house.bids(auction_id).amount_to_sell == Wad( self.debt_auction_bid_size * self.geb.oracle_relayer.redemption_price() / Rad.from_number(1400.0)) # when tx_count = self.web3.eth.getTransactionCount( self.keeper_address.address) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert self.web3.eth.getTransactionCount( self.keeper_address.address) == tx_count def test_should_settle_when_we_won_the_auction(self, auction_id): # given (model, model_factory) = models(self.keeper, auction_id) # when simulate_model_output(model=model, price=Wad.from_number(825.0)) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert self.amount_to_sell_implies_price(auction_id, Wad.from_number(825.0)) prot_before = self.geb.prot.balance_of(self.keeper_address) # when time_travel_by(self.web3, self.debt_auction_house.bid_duration() + 1) # and self.keeper.check_all_auctions() wait_for_other_threads() # then prot_after = self.geb.prot.balance_of(self.keeper_address) assert prot_before < prot_after def test_should_not_settle_when_auction_finished_but_somebody_else_won( self, auction_id): # given prot_before = self.geb.prot.balance_of(self.keeper_address) # and self.decrease_sold_amount(auction_id, self.other_address, Wad.from_number(0.000015), self.debt_auction_bid_size) assert self.debt_auction_house.bids( auction_id).amount_to_sell == Wad.from_number(0.000015) # when time_travel_by(self.web3, self.debt_auction_house.bid_duration() + 1) # and self.keeper.check_all_auctions() wait_for_other_threads() # then prot_after = self.geb.prot.balance_of(self.keeper_address) assert prot_before == prot_after def test_should_obey_gas_price_provided_by_the_model(self, auction_id): # given (model, model_factory) = models(self.keeper, auction_id) # when simulate_model_output(model=model, price=Wad.from_number(800.0), gas_price=175000) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert self.debt_auction_house.bids( auction_id).high_bidder == self.keeper_address assert self.web3.eth.getBlock( 'latest', full_transactions=True).transactions[0].gasPrice == 175000 # cleanup time_travel_by(self.web3, self.debt_auction_house.bid_duration() + 1) assert self.debt_auction_house.settle_auction(auction_id).transact() def test_should_use_default_gas_price_if_not_provided_by_the_model( self, auction_id): # given (model, model_factory) = models(self.keeper, auction_id) # when simulate_model_output(model=model, price=Wad.from_number(850.0)) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert self.debt_auction_house.bids( auction_id).high_bidder == self.keeper_address assert self.web3.eth.getBlock('latest', full_transactions=True).transactions[0].gasPrice == \ self.default_gas_price # cleanup time_travel_by(self.web3, self.debt_auction_house.bid_duration() + 1) assert self.debt_auction_house.settle_auction(auction_id).transact() def test_should_change_gas_strategy_when_model_output_changes( self, auction_id): # given (model, model_factory) = models(self.keeper, auction_id) # when first_bid = Wad.from_number(90) simulate_model_output(model=model, price=first_bid, gas_price=2000) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert self.web3.eth.getBlock( 'latest', full_transactions=True).transactions[0].gasPrice == 2000 # when second_bid = Wad.from_number(100) simulate_model_output(model=model, price=second_bid) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert round( Rad(self.debt_auction_house.bids(auction_id).amount_to_sell), 2) == round(self.debt_auction_bid_size / Rad(second_bid), 2) assert self.web3.eth.getBlock('latest', full_transactions=True).transactions[0].gasPrice == \ self.default_gas_price # when third_bid = Wad.from_number(110) new_gas_price = int(self.default_gas_price * 1.25) simulate_model_output(model=model, price=third_bid, gas_price=new_gas_price) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert round( Rad(self.debt_auction_house.bids(auction_id).amount_to_sell), 2) == round(self.debt_auction_bid_size / Rad(third_bid), 2) assert self.web3.eth.getBlock( 'latest', full_transactions=True).transactions[0].gasPrice == new_gas_price # cleanup time_travel_by(self.web3, self.debt_auction_house.bid_duration() + 1) assert self.debt_auction_house.settle_auction(auction_id).transact() @classmethod def teardown_class(cls): cls.cleanup_debt(get_web3(), get_geb(get_web3()), get_other_address(get_web3())) @classmethod def cleanup_debt(cls, web3, geb, address): # Cancel out debt unqueued_unauctioned_debt = geb.accounting_engine.unqueued_unauctioned_debt( ) total_on_auction_debt = geb.accounting_engine.total_on_auction_debt() system_coin_needed = unqueued_unauctioned_debt + total_on_auction_debt if system_coin_needed == Rad(0): return # Add Wad(1) when going from Rad to Wad reserve_system_coin(geb, geb.collaterals['ETH-A'], get_our_address(web3), Wad(system_coin_needed) + Wad(1)) # transfer system coin to accounting engine geb.safe_engine.transfer_internal_coins( get_our_address(web3), geb.accounting_engine.address, system_coin_needed).transact(from_address=get_our_address(web3)) system_coin_accounting_engine = geb.safe_engine.coin_balance( geb.accounting_engine.address) assert system_coin_accounting_engine >= system_coin_needed assert geb.accounting_engine.settle_debt( unqueued_unauctioned_debt).transact() assert geb.accounting_engine.unqueued_unauctioned_debt() == Rad(0) assert geb.accounting_engine.total_queued_debt() == Rad(0) if geb.accounting_engine.total_on_auction_debt() > Rad(0): geb.accounting_engine.cancel_auctioned_debt_with_surplus( total_on_auction_debt).transact() assert geb.accounting_engine.total_on_auction_debt() == Rad(0) assert geb.safe_engine.debt_balance( geb.accounting_engine.address) == Rad(0)
class TestAuctionKeeperFixedDiscountCollateralAuctionHouse(TransactionIgnoringTest): def teardown_method(self, test_method): pass def setup_class(self): """ I'm excluding initialization of a specific collateral perchance we use multiple collaterals to improve test speeds. This prevents us from instantiating the keeper as a class member. """ self.web3 = get_web3() self.geb = get_geb(self.web3) self.keeper_address = get_keeper_address(self.web3) self.collateral = self.geb.collaterals['ETH-B'] self.min_auction = self.collateral.collateral_auction_house.auctions_started() + 1 self.keeper = AuctionKeeper(args=args(f"--eth-from {self.keeper_address.address} " f"--type collateral " f"--from-block 200 " f"--min-auction {self.min_auction} " f"--model ../models/collateral_model.sh " f"--collateral-type {self.collateral.collateral_type.name}"), web3=self.geb.web3) self.keeper.approve() assert isinstance(self.keeper.gas_price, DynamicGasPrice) self.default_gas_price = self.keeper.gas_price.get_gas_price(0) @staticmethod def collateral_balance(address: Address, c: Collateral) -> Wad: assert (isinstance(address, Address)) assert (isinstance(c, Collateral)) return Wad(c.collateral.balance_of(address)) @staticmethod def buy_collateral(collateral_auction_house: FixedDiscountCollateralAuctionHouse, id: int, address: Address, bid_amount: Wad): assert (isinstance(collateral_auction_house, FixedDiscountCollateralAuctionHouse)) assert (isinstance(id, int)) assert (isinstance(bid_amount, Wad)) current_bid = collateral_auction_house.bids(id) assert current_bid.auction_deadline > datetime.now().timestamp() assert bid_amount <= Wad(current_bid.amount_to_raise) assert collateral_auction_house.buy_collateral(id, bid_amount).transact(from_address=address) @staticmethod def buy_collateral_with_system_coin(geb: GfDeployment, c: Collateral, collateral_auction_house: FixedDiscountCollateralAuctionHouse, id: int, address: Address, bid_amount: Wad): assert (isinstance(geb, GfDeployment)) assert (isinstance(c, Collateral)) assert (isinstance(collateral_auction_house, FixedDiscountCollateralAuctionHouse)) assert (isinstance(id, int)) assert (isinstance(bid_amount, Wad)) collateral_auction_house.approve(collateral_auction_house.safe_engine(), approval_function=approve_safe_modification_directly(from_address=address)) previous_bid = collateral_auction_house.bids(id) c.approve(address) reserve_system_coin(geb, c, address, bid_amount, extra_collateral=Wad.from_number(2)) TestAuctionKeeperFixedDiscountCollateralAuctionHouse.buy_collateral(collateral_auction_house, id, address, bid_amount) def simulate_model_bid(self, geb: GfDeployment, c: Collateral, model: object, gas_price: Optional[int] = None): assert (isinstance(geb, GfDeployment)) assert (isinstance(c, Collateral)) assert (isinstance(gas_price, int)) or gas_price is None collateral_auction_house = c.collateral_auction_house initial_bid = collateral_auction_house.bids(model.id) assert initial_bid.amount_to_sell > Wad(0) our_bid = Wad.from_number(500) * initial_bid.amount_to_sell reserve_system_coin(geb, c, self.keeper_address, our_bid, extra_collateral=Wad.from_number(2)) simulate_model_output(model=model, price=Wad.from_number(500), gas_price=gas_price) def test_collateral_auction_house_address(self): """ Sanity check ensures the keeper fixture is looking at the correct collateral """ assert self.keeper.collateral_auction_house.address == self.collateral.collateral_auction_house.address def test_should_start_a_new_model_and_provide_it_with_info_on_auction_start(self, auction_id, other_address): # given collateral_auction_house = self.collateral.collateral_auction_house if not isinstance(collateral_auction_house, FixedDiscountCollateralAuctionHouse): return (model, model_factory) = models(self.keeper, auction_id) # when self.keeper.check_all_auctions() wait_for_other_threads() initial_bid = self.collateral.collateral_auction_house.bids(auction_id) # then model_factory.create_model.assert_called_once_with(Parameters(collateral_auction_house=collateral_auction_house.address, surplus_auction_house=None, debt_auction_house=None, id=auction_id)) # and status = model.send_status.call_args[0][0] assert status.id == auction_id assert status.collateral_auction_house == collateral_auction_house.address assert status.surplus_auction_house is None assert status.debt_auction_house is None assert status.amount_to_sell == initial_bid.amount_to_sell assert status.amount_to_raise == initial_bid.amount_to_raise assert status.block_time > 0 assert status.auction_deadline < status.block_time + collateral_auction_house.total_auction_length() + 1 # cleanup TestAuctionKeeperFixedDiscountCollateralAuctionHouse.buy_collateral_with_system_coin(self.geb, self.collateral, collateral_auction_house, auction_id, other_address, Wad.from_number(30)) def test_should_provide_model_with_updated_info_after_our_partial_bid(self, auction_id): # given collateral_auction_house = self.collateral.collateral_auction_house if not isinstance(collateral_auction_house, FixedDiscountCollateralAuctionHouse): return (model, model_factory) = models(self.keeper, auction_id) # when self.keeper.check_all_auctions() wait_for_other_threads() initial_status = collateral_auction_house.bids(model.id) # then assert model.send_status.call_count == 1 # when bidding less than the full amount our_balance = Wad(initial_status.amount_to_raise) / Wad.from_number(2) reserve_system_coin(self.geb, self.collateral, self.keeper_address, our_balance) assert initial_status.amount_to_raise != Rad(0) assert self.geb.safe_engine.coin_balance(self.keeper_address) > Rad(0) # Make our balance lte half of the auction size half_amount_to_raise = initial_status.amount_to_raise / Rad.from_number(2) if self.geb.safe_engine.coin_balance(self.keeper_address) >= half_amount_to_raise: burn_amount = self.geb.safe_engine.coin_balance(self.keeper_address) - half_amount_to_raise assert burn_amount < self.geb.safe_engine.coin_balance(self.keeper_address) self.geb.safe_engine.transfer_internal_coins(self.keeper_address, Address("0x0000000000000000000000000000000000000000"), burn_amount).transact() assert self.geb.safe_engine.coin_balance(self.keeper_address) <= half_amount_to_raise assert self.geb.safe_engine.coin_balance(self.keeper_address) > Rad(0) simulate_model_output(model=model, price=None) self.keeper.check_for_bids() # and checking auction status and sending auction status to model self.keeper.check_all_auctions() wait_for_other_threads() # and self.keeper.check_all_auctions() wait_for_other_threads() # then assert model.send_status.call_count > 1 # ensure our bid was processed current_status = collateral_auction_house.bids(model.id) assert current_status.amount_to_raise == initial_status.amount_to_raise assert current_status.amount_to_sell == initial_status.amount_to_sell assert current_status.auction_deadline == initial_status.auction_deadline assert current_status.raised_amount == Rad(our_balance) # and the last status sent to our model reflects our bid status = model.send_status.call_args[0][0] assert status.id == auction_id assert status.collateral_auction_house == collateral_auction_house.address assert status.surplus_auction_house is None assert status.debt_auction_house is None assert status.amount_to_sell == initial_status.amount_to_sell assert status.amount_to_raise == initial_status.amount_to_raise assert status.raised_amount == Rad(our_balance) assert status.auction_deadline == initial_status.auction_deadline # and auction is still active final_status = collateral_auction_house.bids(model.id) assert final_status.amount_to_raise == initial_status.amount_to_raise assert final_status.amount_to_sell == initial_status.amount_to_sell assert final_status.auction_deadline == initial_status.auction_deadline assert final_status.raised_amount == Rad(our_balance) #cleanup our_balance = Wad(initial_status.amount_to_raise) + Wad(1) reserve_system_coin(self.geb, self.collateral, self.keeper_address, our_balance) assert self.geb.safe_engine.coin_balance(self.keeper_address) >= initial_status.amount_to_raise simulate_model_output(model=model, price=None) self.keeper.check_for_bids() self.keeper.check_all_auctions() wait_for_other_threads() # ensure auction has been deleted current_status = collateral_auction_house.bids(model.id) assert current_status.raised_amount == Rad(0) assert current_status.sold_amount == Wad(0) assert current_status.amount_to_raise == Rad(0) assert current_status.amount_to_sell == Wad(0) assert current_status.auction_deadline == 0 assert current_status.raised_amount == Rad(0) def test_auction_deleted_after_our_full_bid(self, auction_id): # given collateral_auction_house = self.collateral.collateral_auction_house if not isinstance(collateral_auction_house, FixedDiscountCollateralAuctionHouse): return (model, model_factory) = models(self.keeper, auction_id) # when self.keeper.check_all_auctions() wait_for_other_threads() initial_status = collateral_auction_house.bids(model.id) # then assert model.send_status.call_count == 1 # when bidding the full amount our_bid = Wad(initial_status.amount_to_raise) + Wad(1) reserve_system_coin(self.geb, self.collateral, self.keeper_address, our_bid, Wad.from_number(2)) assert self.geb.safe_engine.coin_balance(self.keeper_address) >= initial_status.amount_to_raise simulate_model_output(model=model, price=None) self.keeper.check_for_bids() # and checking auction status and sending auction status to model self.keeper.check_all_auctions() wait_for_other_threads() # and self.keeper.check_all_auctions() wait_for_other_threads() # ensure our bid was processed and auction has been deleted current_status = collateral_auction_house.bids(model.id) assert current_status.raised_amount == Rad(0) assert current_status.sold_amount == Wad(0) assert current_status.amount_to_raise == Rad(0) assert current_status.amount_to_sell == Wad(0) assert current_status.auction_deadline == 0 assert current_status.raised_amount == Rad(0) def test_should_provide_model_with_updated_info_after_somebody_else_partial_bids(self, auction_id, other_address): # given collateral_auction_house = self.collateral.collateral_auction_house if not isinstance(collateral_auction_house, FixedDiscountCollateralAuctionHouse): return (model, model_factory) = models(self.keeper, auction_id) # when self.keeper.check_all_auctions() wait_for_other_threads() # then assert model.send_status.call_count == 1 # when collateral_auction_house.approve(collateral_auction_house.safe_engine(), approval_function=approve_safe_modification_directly(from_address=other_address)) previous_bid = collateral_auction_house.bids(auction_id) new_bid_amount = Wad.from_number(30) self.buy_collateral_with_system_coin(self.geb, self.collateral, collateral_auction_house, model.id, other_address, new_bid_amount) # and self.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 == auction_id assert status.collateral_auction_house == collateral_auction_house.address assert status.surplus_auction_house is None assert status.debt_auction_house is None assert status.raised_amount == Rad(new_bid_amount) assert status.amount_to_sell == previous_bid.amount_to_sell assert status.amount_to_raise == previous_bid.amount_to_raise assert status.block_time > 0 assert status.auction_deadline > status.block_time def test_should_not_do_anything_if_no_output_from_model(self): # given collateral_auction_house = self.collateral.collateral_auction_house if not isinstance(collateral_auction_house, FixedDiscountCollateralAuctionHouse): return previous_block_number = self.web3.eth.blockNumber # when # [no output from model] # and self.keeper.check_all_auctions() wait_for_other_threads() # then assert self.web3.eth.blockNumber == previous_block_number @pytest.mark.skip("failing after adding rebalance_system_coin() in check_bids") def test_should_increase_gas_price_of_pending_transactions_if_model_increases_gas_price(self, auction_id): # given collateral_auction_house = self.collateral.collateral_auction_house if not isinstance(collateral_auction_house, FixedDiscountCollateralAuctionHouse): return (model, model_factory) = models(self.keeper, auction_id) # when bid_price = Wad.from_number(20.0) reserve_system_coin(self.geb, self.collateral, self.keeper_address, bid_price * bid_size * 2, Wad.from_number(2)) self.keeper.rebalance_system_coin() simulate_model_output(model=model, price=bid_price, 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=bid_price, gas_price=15) # and self.keeper.check_for_bids() wait_for_other_threads() # then #assert collateral_auction_house.bids(auction_id).raised_amount == Rad(bid_price * bid_size) assert self.web3.eth.getBlock('latest', full_transactions=True).transactions[0].gasPrice == 15 def test_should_obey_gas_price_provided_by_the_model(self, auction_id): # given collateral_auction_house = self.collateral.collateral_auction_house if not isinstance(collateral_auction_house, FixedDiscountCollateralAuctionHouse): return (model, model_factory) = models(self.keeper, auction_id) # when self.simulate_model_bid(self.geb, self.collateral, model, gas_price=175000) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert self.web3.eth.getBlock('latest', full_transactions=True).transactions[0].gasPrice == 175000 def test_should_use_default_gas_price_if_not_provided_by_the_model(self, auction_id): # given collateral_auction_house = self.collateral.collateral_auction_house if not isinstance(collateral_auction_house, FixedDiscountCollateralAuctionHouse): return (model, model_factory) = models(self.keeper, auction_id) # when self.simulate_model_bid(self.geb, self.collateral, model) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert self.web3.eth.getBlock('latest', full_transactions=True).transactions[0].gasPrice == \ self.default_gas_price @classmethod def teardown_class(cls): pop_debt_and_settle_debt(get_web3(), get_geb(get_web3()), past_blocks=1200, require_settle_debt=True) cls.cleanup_debt(get_web3(), get_geb(get_web3()), get_other_address(get_web3())) @classmethod def cleanup_debt(cls, web3, geb, address): # Cancel out debt unqueued_unauctioned_debt = geb.accounting_engine.unqueued_unauctioned_debt() total_on_auction_debt = geb.accounting_engine.total_on_auction_debt() system_coin_needed = unqueued_unauctioned_debt + total_on_auction_debt #system_coin_needed = geb.safe_engine.debt_balance(geb.accounting_engine.address) if system_coin_needed == Rad(0): return # Need to add Wad(1) when going from Rad to Wad reserve_system_coin(geb, geb.collaterals['ETH-A'], get_our_address(web3), Wad(system_coin_needed) + Wad(1)) assert geb.safe_engine.coin_balance(get_our_address(web3)) >= system_coin_needed # transfer system coin to accounting engine geb.safe_engine.transfer_internal_coins(get_our_address(web3), geb.accounting_engine.address, system_coin_needed).transact(from_address=get_our_address(web3)) system_coin_accounting_engine = geb.safe_engine.coin_balance(geb.accounting_engine.address) assert system_coin_accounting_engine >= system_coin_needed assert geb.accounting_engine.settle_debt(unqueued_unauctioned_debt).transact() assert geb.accounting_engine.unqueued_unauctioned_debt() == Rad(0) assert geb.accounting_engine.total_queued_debt() == Rad(0) if geb.accounting_engine.total_on_auction_debt() > Rad(0): geb.accounting_engine.cancel_auctioned_debt_with_surplus(total_on_auction_debt).transact() assert geb.accounting_engine.total_on_auction_debt() == Rad(0) assert geb.safe_engine.debt_balance(geb.accounting_engine.address) == Rad(0)
class TestAuctionKeeperFlipper(TransactionIgnoringTest): def setup_class(self): """ I'm excluding initialization of a specific collateral perchance we use multiple collaterals to improve test speeds. This prevents us from instantiating the keeper as a class member. """ self.web3 = web3() self.mcd = mcd(self.web3) self.keeper_address = keeper_address(self.web3) self.collateral = self.mcd.collaterals['ETH-B'] self.keeper = AuctionKeeper(args=args( f"--eth-from {self.keeper_address.address} " f"--type flip " f"--from-block 1 " f"--ilk {self.collateral.ilk.name} " f"--model ./bogus-model.sh"), web3=self.mcd.web3) self.keeper.approve() assert isinstance(self.keeper.gas_price, DynamicGasPrice) self.default_gas_price = self.keeper.gas_price.get_gas_price(0) @staticmethod def gem_balance(address: Address, c: Collateral) -> Wad: assert (isinstance(address, Address)) assert (isinstance(c, Collateral)) return Wad(c.gem.balance_of(address)) 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) @staticmethod def tend(flipper: Flipper, id: int, address: Address, lot: Wad, bid: Rad): assert (isinstance(flipper, Flipper)) assert (isinstance(id, int)) assert (isinstance(lot, Wad)) assert (isinstance(bid, Rad)) current_bid = flipper.bids(id) assert current_bid.guy != Address( "0x0000000000000000000000000000000000000000") assert current_bid.tic > datetime.now().timestamp( ) or current_bid.tic == 0 assert current_bid.end > datetime.now().timestamp() assert lot == current_bid.lot assert bid <= current_bid.tab assert bid > current_bid.bid assert (bid >= Rad(flipper.beg()) * current_bid.bid) or ( bid == current_bid.tab) assert flipper.tend(id, lot, bid).transact(from_address=address) @staticmethod def dent(flipper: Flipper, id: int, address: Address, lot: Wad, bid: Rad): assert (isinstance(flipper, Flipper)) assert (isinstance(id, int)) assert (isinstance(lot, Wad)) assert (isinstance(bid, Rad)) current_bid = flipper.bids(id) assert current_bid.guy != Address( "0x0000000000000000000000000000000000000000") assert current_bid.tic > datetime.now().timestamp( ) or current_bid.tic == 0 assert current_bid.end > datetime.now().timestamp() assert bid == current_bid.bid assert bid == current_bid.tab assert lot < current_bid.lot assert flipper.beg() * lot <= current_bid.lot assert flipper.dent(id, lot, bid).transact(from_address=address) @staticmethod 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 test_flipper_address(self): """ Sanity check ensures the keeper fixture is looking at the correct collateral """ assert self.keeper.flipper.address == self.collateral.flipper.address def test_should_start_a_new_model_and_provide_it_with_info_on_auction_kick( self, kick, other_address): # given (model, model_factory) = models(self.keeper, kick) flipper = self.collateral.flipper # when self.keeper.check_all_auctions() wait_for_other_threads() initial_bid = self.collateral.flipper.bids(kick) # then model_factory.create_model.assert_called_once_with( Parameters(flipper=flipper.address, flapper=None, flopper=None, id=kick)) # 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.from_number(0) assert status.lot == initial_bid.lot assert status.tab == initial_bid.tab assert status.beg > Wad.from_number(1) assert status.guy == self.mcd.cat.address assert status.era > 0 assert status.end < status.era + flipper.tau() + 1 assert status.tic == 0 assert status.price == Wad(0) # cleanup time_travel_by(self.web3, flipper.ttl() + 1) self.keeper.check_all_auctions() TestAuctionKeeperFlipper.tend_with_dai(self.mcd, self.collateral, flipper, kick, other_address, Rad.from_number(80)) flipper.deal(kick).transact(from_address=other_address) def test_should_provide_model_with_updated_info_after_our_own_bid( self, kick): # given flipper = self.collateral.flipper (model, model_factory) = models(self.keeper, kick) # when self.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(self.mcd, self.collateral, self.keeper_address, our_bid) simulate_model_output(model=model, price=our_price) self.keeper.check_for_bids() # and self.keeper.check_all_auctions() wait_for_other_threads() # and self.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 test_should_provide_model_with_updated_info_after_somebody_else_bids( self, kick, other_address): # given flipper = self.collateral.flipper (model, model_factory) = models(self.keeper, kick) # when self.keeper.check_all_auctions() wait_for_other_threads() # then assert model.send_status.call_count == 1 # when flipper.approve( flipper.vat(), approval_function=hope_directly(from_address=other_address)) previous_bid = flipper.bids(kick) new_bid_amount = Rad.from_number(80) self.tend_with_dai(self.mcd, self.collateral, flipper, model.id, other_address, new_bid_amount) # and self.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 == new_bid_amount assert status.lot == previous_bid.lot assert status.tab == previous_bid.tab assert status.beg > Wad.from_number(1) assert status.guy == other_address assert status.era > 0 assert status.end > status.era assert status.tic > status.era assert status.price == (Wad(new_bid_amount) / previous_bid.lot) def test_should_tick_if_auction_expired_due_to_tau(self, kick): # given flipper = self.collateral.flipper (model, model_factory) = models(self.keeper, kick) # when self.keeper.check_all_auctions() wait_for_other_threads() # then model_factory.create_model.assert_called_once() model.terminate.assert_not_called() # when time_travel_by(self.web3, flipper.tau() + 1) # and self.simulate_model_bid(self.mcd, self.collateral, model, Wad.from_number(15.0)) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then model.terminate.assert_not_called() auction = flipper.bids(kick) assert round(auction.bid / Rad(auction.lot), 2) == round(Rad.from_number(15.0), 2) # cleanup time_travel_by(self.web3, flipper.ttl() + 1) self.keeper.check_all_auctions() model.terminate.assert_called_once() def test_should_terminate_model_if_auction_expired_due_to_ttl_and_somebody_else_won_it( self, kick, other_address): # given (model, model_factory) = models(self.keeper, kick) flipper = self.collateral.flipper # when self.keeper.check_all_auctions() wait_for_other_threads() # then model_factory.create_model.assert_called_once() model.terminate.assert_not_called() # when flipper.approve( flipper.vat(), approval_function=hope_directly(from_address=other_address)) new_bid_amount = Rad.from_number(85) self.tend_with_dai(self.mcd, self.collateral, flipper, kick, other_address, new_bid_amount) # and time_travel_by(self.web3, flipper.ttl() + 1) # and self.keeper.check_all_auctions() wait_for_other_threads() # then model_factory.create_model.assert_called_once() model.terminate.assert_called_once() # cleanup assert flipper.deal(kick).transact() def test_should_terminate_model_if_auction_is_dealt( self, kick, other_address): # given (model, model_factory) = models(self.keeper, kick) flipper = self.collateral.flipper # when self.keeper.check_all_auctions() wait_for_other_threads() # then model_factory.create_model.assert_called_once() model.terminate.assert_not_called() # when self.tend_with_dai(self.mcd, self.collateral, flipper, kick, other_address, Rad.from_number(90)) # and time_travel_by(self.web3, flipper.ttl() + 1) # and flipper.deal(kick).transact(from_address=other_address) # and self.keeper.check_all_auctions() wait_for_other_threads() # then model_factory.create_model.assert_called_once() model.terminate.assert_called_once() def test_should_not_instantiate_model_if_auction_is_dealt( self, kick, other_address): # given (model, model_factory) = models(self.keeper, kick) flipper = self.collateral.flipper # and self.tend_with_dai(self.mcd, self.collateral, flipper, kick, other_address, Rad.from_number(90)) # and time_travel_by(self.web3, flipper.ttl() + 1) # and flipper.deal(kick).transact(from_address=other_address) # when self.keeper.check_all_auctions() wait_for_other_threads() # then model_factory.create_model.assert_not_called() def test_should_not_do_anything_if_no_output_from_model(self): # given previous_block_number = self.web3.eth.blockNumber # when # [no output from model] # and self.keeper.check_all_auctions() wait_for_other_threads() # then assert self.web3.eth.blockNumber == previous_block_number def test_should_make_initial_bid(self, kick): # given (model, model_factory) = models(self.keeper, kick) flipper = self.collateral.flipper # when self.simulate_model_bid(self.mcd, self.collateral, model, Wad.from_number(16.0)) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then auction = flipper.bids(kick) assert round(auction.bid / Rad(auction.lot), 2) == round(Rad.from_number(16.0), 2) # cleanup time_travel_by(self.web3, flipper.ttl() + 1) assert flipper.deal(kick).transact() def test_should_bid_even_if_there_is_already_a_bidder( self, kick, other_address): # given (model, model_factory) = models(self.keeper, kick) flipper = self.collateral.flipper # and self.tend_with_dai(self.mcd, self.collateral, flipper, kick, other_address, Rad.from_number(21)) assert flipper.bids(kick).bid == Rad.from_number(21) # when self.simulate_model_bid(self.mcd, self.collateral, model, Wad.from_number(23)) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then auction = flipper.bids(kick) assert round(auction.bid / Rad(auction.lot), 2) == round(Rad.from_number(23), 2) 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_use_most_up_to_date_price_for_dent_even_if_it_gets_updated_during_tend( self, kick): # given (model, model_factory) = models(self.keeper, kick) flipper = self.collateral.flipper # when first_bid_price = Wad.from_number(140) self.simulate_model_bid(self.mcd, self.collateral, model, first_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 second_bid_price = Wad.from_number(150) self.simulate_model_bid(self.mcd, self.collateral, model, second_bid_price) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() auction = flipper.bids(kick) assert auction.bid == auction.tab assert auction.lot == Wad(auction.bid / Rad(second_bid_price)) # cleanup time_travel_by(self.web3, flipper.ttl() + 1) assert flipper.deal(kick).transact() def test_should_only_tend_if_bid_is_only_slightly_above_tab(self, kick): # given (model, model_factory) = models(self.keeper, kick) flipper = self.collateral.flipper # when auction = flipper.bids(kick) bid_price = Wad(auction.tab) + Wad.from_number(0.1) self.simulate_model_bid(self.mcd, self.collateral, model, 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 self.keeper.check_all_auctions() wait_for_other_threads() # then auction = flipper.bids(kick) assert auction.bid == auction.tab assert auction.lot == tend_lot # cleanup time_travel_by(self.web3, flipper.ttl() + 1) assert flipper.deal(kick).transact() def test_should_tend_up_to_exactly_tab_if_bid_is_only_slightly_below_tab( self, kick): """I assume the point of this test is that the bid increment should be ignored when `tend`ing the `tab` to transition the auction into _dent_ phase.""" # given (model, model_factory) = models(self.keeper, kick) flipper = self.collateral.flipper # when auction = flipper.bids(kick) assert auction.bid == Rad(0) bid_price = Wad(auction.tab / Rad(tend_lot)) - Wad.from_number(0.01) self.simulate_model_bid(self.mcd, self.collateral, model, 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 round(auction.bid, 2) == round(Rad(bid_price * tend_lot), 2) assert auction.lot == tend_lot # when price_to_reach_tab = Wad(auction.tab / Rad(tend_lot)) + Wad(1) self.simulate_model_bid(self.mcd, self.collateral, model, price_to_reach_tab) # 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 # cleanup time_travel_by(self.web3, flipper.ttl() + 1) assert flipper.deal(kick).transact() def test_should_overbid_itself_if_model_has_updated_the_price(self, kick): # given (model, model_factory) = models(self.keeper, kick) flipper = self.collateral.flipper # when first_bid = Wad.from_number(15.0) self.simulate_model_bid(self.mcd, self.collateral, model, first_bid) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert flipper.bids(kick).bid == Rad(first_bid * tend_lot) # when second_bid = Wad.from_number(20.0) self.simulate_model_bid(self.mcd, self.collateral, model, second_bid) self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert flipper.bids(kick).bid == Rad(second_bid * tend_lot) # cleanup time_travel_by(self.web3, flipper.ttl() + 1) assert flipper.deal(kick).transact() def test_should_increase_gas_price_of_pending_transactions_if_model_increases_gas_price( self, kick): # given (model, model_factory) = models(self.keeper, kick) flipper = self.collateral.flipper # when bid_price = Wad.from_number(20.0) reserve_dai(self.mcd, self.collateral, 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 self.keeper.check_all_auctions() self.keeper.check_for_bids() # and self.end_ignoring_transactions() # and simulate_model_output(model=model, price=bid_price, gas_price=15) # and self.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 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 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 test_should_deal_when_we_won_the_auction(self, kick): # given flipper = self.collateral.flipper # when collateral_before = self.collateral.gem.balance_of(self.keeper_address) # when time_travel_by(self.web3, flipper.ttl() + 1) lot_won = flipper.bids(kick).lot assert lot_won > Wad(0) # and self.keeper.check_all_auctions() wait_for_other_threads() assert self.collateral.adapter.exit( self.keeper_address, lot_won).transact(from_address=self.keeper_address) # then collateral_after = self.collateral.gem.balance_of(self.keeper_address) assert collateral_before < collateral_after def test_should_not_deal_when_auction_finished_but_somebody_else_won( self, kick, other_address): # given flipper = self.collateral.flipper # and bid = Rad.from_number(66) self.tend_with_dai(self.mcd, self.collateral, flipper, kick, other_address, bid) assert flipper.bids(kick).bid == bid # when time_travel_by(self.web3, flipper.ttl() + 1) # and self.keeper.check_all_auctions() wait_for_other_threads() # then ensure the bid hasn't been deleted assert flipper.bids(kick).bid == bid # cleanup assert flipper.deal(kick).transact() assert flipper.bids(kick).bid == Rad(0) def test_should_obey_gas_price_provided_by_the_model(self, kick): # given (model, model_factory) = models(self.keeper, kick) # when self.simulate_model_bid(self.mcd, self.collateral, model, price=Wad.from_number(15.0), gas_price=175000) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert self.collateral.flipper.bids(kick).bid == Rad( Wad.from_number(15.0) * tend_lot) assert self.web3.eth.getBlock( 'latest', full_transactions=True).transactions[0].gasPrice == 175000 def test_should_use_default_gas_price_if_not_provided_by_the_model( self, kick): # given flipper = self.collateral.flipper (model, model_factory) = models(self.keeper, kick) # when self.simulate_model_bid(self.mcd, self.collateral, model, price=Wad.from_number(16.0)) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert flipper.bids(kick).bid == Rad(Wad.from_number(16.0) * tend_lot) assert self.web3.eth.getBlock('latest', full_transactions=True).transactions[0].gasPrice == \ self.default_gas_price # cleanup time_travel_by(self.web3, flipper.ttl() + 1) assert flipper.deal(kick).transact() def test_should_change_gas_strategy_when_model_output_changes(self, kick): # given flipper = self.collateral.flipper (model, model_factory) = models(self.keeper, kick) # when first_bid = Wad.from_number(3) self.simulate_model_bid(self.mcd, self.collateral, model=model, price=first_bid, gas_price=2000) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert self.web3.eth.getBlock( 'latest', full_transactions=True).transactions[0].gasPrice == 2000 # when second_bid = Wad.from_number(6) self.simulate_model_bid(self.mcd, self.collateral, model=model, price=second_bid) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert flipper.bids(kick).bid == Rad(second_bid * tend_lot) assert self.web3.eth.getBlock('latest', full_transactions=True).transactions[0].gasPrice == \ self.default_gas_price # when third_bid = Wad.from_number(9) new_gas_price = int(self.default_gas_price * 1.25) self.simulate_model_bid(self.mcd, self.collateral, model=model, price=third_bid, gas_price=new_gas_price) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert flipper.bids(kick).bid == Rad(third_bid * tend_lot) assert self.web3.eth.getBlock( 'latest', full_transactions=True).transactions[0].gasPrice == new_gas_price # cleanup time_travel_by(self.web3, flipper.ttl() + 1) assert flipper.deal(kick).transact() @classmethod def teardown_class(cls): flog_and_heal(web3(), mcd(web3()), past_blocks=1200, require_heal=False)
class TestAuctionKeeperSurplus(TransactionIgnoringTest): def setup_method(self): self.web3 = get_web3() self.our_address = get_our_address(self.web3) self.keeper_address = get_keeper_address(self.web3) self.other_address = get_other_address(self.web3) self.auction_income_recipient_address = get_auction_income_recipient_address(self.web3) self.geb = get_geb(self.web3) self.surplus_auction_house = self.geb.surplus_auction_house self.surplus_auction_house.approve(self.geb.prot.address, directly(from_address=self.other_address)) #self.min_auction = self.geb.surplus_auction_house.auctions_started() + 1 self.keeper = AuctionKeeper(args=args(f"--eth-from {self.keeper_address} " f"--type surplus " f"--from-block 1 " f"--bid-delay 0 " #f"--min-auction {self.min_auction} " f"--model ./bogus-model.sh"), web3=self.web3) self.keeper.approve() mint_prot(self.geb.prot, self.keeper_address, Wad.from_number(50000)) mint_prot(self.geb.prot, self.other_address, Wad.from_number(50000)) assert isinstance(self.keeper.gas_price, DynamicGasPrice) # Since no args were assigned, gas strategy should return a GeometricGasPrice starting at the node gas price self.default_gas_price = get_node_gas_price(self.web3) def test_should_detect_surplus_auction(self, web3, geb, c, auction_income_recipient_address, keeper_address): print(self.keeper) # given some PROT is available to the keeper and a count of surplus auctions mint_prot(geb.prot, keeper_address, Wad.from_number(50000)) auctions_started = geb.surplus_auction_house.auctions_started() # when surplus is generated create_safe_with_surplus(geb, c, auction_income_recipient_address) self.keeper.check_surplus() for thread in threading.enumerate(): print(thread) wait_for_other_threads() # then ensure another surplus auction was started auction_id = geb.surplus_auction_house.auctions_started() assert auction_id == auctions_started + 1 # clean up by letting someone else bid and waiting until the auction ends auction = self.surplus_auction_house.bids(auction_id) assert self.surplus_auction_house.increase_bid_size(auction_id, auction.amount_to_sell, Wad.from_number(30)).transact(from_address=self.other_address) time_travel_by(web3, geb.surplus_auction_house.bid_duration() + 1) def test_should_start_a_new_model_and_provide_it_with_info_on_auction_start(self, auction_id): # given (model, model_factory) = models(self.keeper, auction_id) # when self.keeper.check_all_auctions() wait_for_other_threads() # then model_factory.create_model.assert_called_once_with(Parameters(collateral_auction_house=None, surplus_auction_house=self.surplus_auction_house.address, debt_auction_house=None, staked_token_auction_house=None, id=auction_id)) # and status = model.send_status.call_args[0][0] assert status.id == auction_id assert status.collateral_auction_house is None assert status.surplus_auction_house == self.surplus_auction_house.address assert status.debt_auction_house is None assert status.bid_amount == Wad(0) assert status.amount_to_sell == self.geb.accounting_engine.surplus_auction_amount_to_sell() assert status.amount_to_raise is None assert status.bid_increase == self.geb.surplus_auction_house.bid_increase() assert status.high_bidder == self.geb.accounting_engine.address assert status.block_time > 0 assert status.auction_deadline < status.block_time + self.surplus_auction_house.total_auction_length() + 1 assert status.bid_expiry == 0 assert status.price is None def test_should_provide_model_with_updated_info_after_our_own_bid(self): # given auction_id = self.surplus_auction_house.auctions_started() (model, model_factory) = models(self.keeper, auction_id) # when self.keeper.check_all_auctions() wait_for_other_threads() # then assert model.send_status.call_count == 1 # when simulate_model_output(model=model, price=Wad.from_number(9)) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # and self.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 == auction_id assert status.collateral_auction_house is None assert status.surplus_auction_house == self.surplus_auction_house.address assert status.debt_auction_house is None assert status.bid_amount == Wad(self.surplus_auction_house.bids(auction_id).amount_to_sell * self.geb.oracle_relayer.redemption_price() / Rad.from_number(9)) assert status.amount_to_sell == self.geb.accounting_engine.surplus_auction_amount_to_sell() assert status.amount_to_raise is None assert status.bid_increase == self.geb.surplus_auction_house.bid_increase() assert status.high_bidder == self.keeper_address assert status.block_time > 0 assert status.auction_deadline > status.block_time assert status.bid_expiry > status.block_time assert round(status.price, 2) == round(Wad.from_number(9), 2) # cleanup time_travel_by(self.web3, self.surplus_auction_house.bid_duration() + 1) assert self.surplus_auction_house.settle_auction(auction_id).transact() def test_should_provide_model_with_updated_info_after_somebody_else_bids(self, auction_id): # given (model, model_factory) = models(self.keeper, auction_id) # when self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert model.send_status.call_count == 1 # when auction = self.surplus_auction_house.bids(auction_id) assert Wad.from_number(40) > auction.bid_amount assert self.surplus_auction_house.increase_bid_size(auction_id, auction.amount_to_sell, Wad.from_number(40)).transact(from_address=self.other_address) # and self.keeper.check_all_auctions() wait_for_other_threads() auction = self.surplus_auction_house.bids(auction_id) # then assert model.send_status.call_count > 1 # and status = model.send_status.call_args[0][0] assert status.id == auction_id assert status.collateral_auction_house is None assert status.surplus_auction_house == self.surplus_auction_house.address assert status.debt_auction_house is None assert status.bid_amount == Wad.from_number(40) assert status.amount_to_sell == auction.amount_to_sell assert status.amount_to_raise is None assert status.bid_increase == self.geb.surplus_auction_house.bid_increase() assert status.high_bidder == self.other_address assert status.block_time > 0 assert status.auction_deadline > status.block_time assert status.bid_expiry > status.block_time assert status.price == Wad(auction.amount_to_sell * self.geb.oracle_relayer.redemption_price() / Rad(auction.bid_amount)) # cleanup time_travel_by(self.web3, self.surplus_auction_house.bid_duration() + 1) assert self.surplus_auction_house.settle_auction(auction_id).transact() def test_should_restart_auction_if_auction_expired_due_to_total_auction_length(self, auction_id): # given (model, model_factory) = models(self.keeper, auction_id) # when self.keeper.check_all_auctions() wait_for_other_threads() # then model_factory.create_model.assert_called_once() model.terminate.assert_not_called() # when time_travel_by(self.web3, self.surplus_auction_house.total_auction_length() + 1) # and simulate_model_output(model=model, price=Wad.from_number(9.0)) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then model.terminate.assert_not_called() auction = self.surplus_auction_house.bids(auction_id) assert round(Wad(auction.amount_to_sell * self.geb.oracle_relayer.redemption_price() ) / auction.bid_amount, 2) == round(Wad.from_number(9.0), 2) # cleanup time_travel_by(self.web3, self.surplus_auction_house.bid_duration() + 1) model_factory.create_model.assert_called_once() self.keeper.check_all_auctions() model.terminate.assert_called_once() def test_should_terminate_model_if_auction_expired_due_to_bid_duration_and_somebody_else_won_it(self, auction_id): # given (model, model_factory) = models(self.keeper, auction_id) # when self.keeper.check_all_auctions() wait_for_other_threads() # then model_factory.create_model.assert_called_once() model.terminate.assert_not_called() # when auction = self.surplus_auction_house.bids(auction_id) assert self.surplus_auction_house.increase_bid_size(auction_id, auction.amount_to_sell, Wad.from_number(40)).transact(from_address=self.other_address) # and time_travel_by(self.web3, self.surplus_auction_house.bid_duration() + 1) # and self.keeper.check_all_auctions() wait_for_other_threads() # then model_factory.create_model.assert_called_once() model.terminate.assert_called_once() def test_should_terminate_model_if_auction_is_settled(self, auction_id): # given auction_id = self.surplus_auction_house.auctions_started() (model, model_factory) = models(self.keeper, auction_id) # when self.keeper.check_all_auctions() wait_for_other_threads() # then model_factory.create_model.assert_called_once() model.terminate.assert_not_called() # when auction = self.surplus_auction_house.bids(auction_id) assert self.surplus_auction_house.increase_bid_size(auction_id, auction.amount_to_sell, Wad.from_number(40)).transact(from_address=self.other_address) # and time_travel_by(self.web3, self.surplus_auction_house.bid_duration() + 1) # and assert self.surplus_auction_house.settle_auction(auction_id).transact(from_address=self.other_address) # and self.keeper.check_all_auctions() wait_for_other_threads() # then model_factory.create_model.assert_called_once() model.terminate.assert_called_once() def test_should_not_instantiate_model_if_auction_is_settled(self, auction_id): # given (model, model_factory) = models(self.keeper, auction_id) # and auction = self.surplus_auction_house.bids(auction_id) self.surplus_auction_house.increase_bid_size(auction_id, auction.amount_to_sell, Wad.from_number(40)).transact(from_address=self.other_address) # and time_travel_by(self.web3, self.surplus_auction_house.bid_duration() + 1) # and self.surplus_auction_house.settle_auction(auction_id).transact(from_address=self.other_address) # when self.keeper.check_all_auctions() wait_for_other_threads() # then model_factory.create_model.assert_not_called() def test_should_not_do_anything_if_no_output_from_model(self, auction_id): # given previous_block_number = self.web3.eth.blockNumber # when # [no output from model] # and self.keeper.check_all_auctions() wait_for_other_threads() # then assert self.web3.eth.blockNumber == previous_block_number def test_should_make_initial_bid(self): # given auction_id = self.surplus_auction_house.auctions_started() (model, model_factory) = models(self.keeper, auction_id) # when simulate_model_output(model=model, price=Wad.from_number(10.0)) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then auction = self.surplus_auction_house.bids(auction_id) assert round(Wad(auction.amount_to_sell * self.geb.oracle_relayer.redemption_price() ) / auction.bid_amount, 2) == round(Wad.from_number(10.0), 2) # cleanup time_travel_by(self.web3, self.surplus_auction_house.bid_duration() + 1) assert self.surplus_auction_house.settle_auction(auction_id).transact() def test_should_bid_even_if_there_is_already_a_bidder(self, auction_id): # given (model, model_factory) = models(self.keeper, auction_id) # and auction = self.surplus_auction_house.bids(auction_id) assert self.surplus_auction_house.increase_bid_size(auction_id, auction.amount_to_sell, Wad.from_number(16)).transact(from_address=self.other_address) assert self.surplus_auction_house.bids(auction_id).bid_amount == Wad.from_number(16) # when simulate_model_output(model=model, price=Wad.from_number(0.0000005)) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then auction = self.surplus_auction_house.bids(auction_id) assert round(Wad(auction.amount_to_sell) / auction.bid_amount, 2) == round(Wad.from_number(0.0000005), 2) # cleanup time_travel_by(self.web3, self.surplus_auction_house.bid_duration() + 1) assert self.surplus_auction_house.settle_auction(auction_id).transact() def test_should_outbid_a_zero_bid(self, auction_id): # given (model, model_factory) = models(self.keeper, auction_id) # and auction = self.surplus_auction_house.bids(auction_id) assert self.surplus_auction_house.increase_bid_size(auction_id, auction.amount_to_sell, Wad(1)).transact(from_address=self.other_address) assert self.surplus_auction_house.bids(auction_id).bid_amount == Wad(1) # when simulate_model_output(model=model, price=Wad.from_number(0.0000006)) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then auction = self.surplus_auction_house.bids(auction_id) assert round(Wad(auction.amount_to_sell) / auction.bid_amount, 2) == round(Wad.from_number(0.0000006), 2) # cleanup time_travel_by(self.web3, self.surplus_auction_house.bid_duration() + 1) assert self.surplus_auction_house.settle_auction(auction_id).transact() def test_should_overbid_itself_if_model_has_updated_the_price(self, auction_id): # given (model, model_factory) = models(self.keeper, auction_id) amount_to_sell = self.surplus_auction_house.bids(auction_id).amount_to_sell # when first_bid = Wad.from_number(0.0000004) simulate_model_output(model=model, price=first_bid) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert self.surplus_auction_house.bids(auction_id).bid_amount == Wad(amount_to_sell * self.geb.oracle_relayer.redemption_price() / Rad(first_bid)) # when second_bid = Wad.from_number(0.0000003) simulate_model_output(model=model, price=second_bid) self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert self.surplus_auction_house.bids(auction_id).bid_amount == Wad(amount_to_sell * self.geb.oracle_relayer.redemption_price() / Rad(second_bid)) # cleanup time_travel_by(self.web3, self.surplus_auction_house.bid_duration() + 1) assert self.surplus_auction_house.settle_auction(auction_id).transact() def test_should_increase_gas_price_of_pending_transactions_if_model_increases_gas_price(self, auction_id): # given (model, model_factory) = models(self.keeper, auction_id) amount_to_sell = self.surplus_auction_house.bids(auction_id).amount_to_sell # when simulate_model_output(model=model, price=Wad.from_number(10.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(10.0), gas_price=15) # and self.keeper.check_for_bids() wait_for_other_threads() # then assert self.surplus_auction_house.bids(auction_id).bid_amount == Wad(amount_to_sell * self.geb.oracle_relayer.redemption_price()) / Wad.from_number(10.0) assert self.web3.eth.getBlock('latest', full_transactions=True).transactions[0].gasPrice == 15 # cleanup time_travel_by(self.web3, self.surplus_auction_house.bid_duration() + 1) assert self.surplus_auction_house.settle_auction(auction_id).transact() def test_should_replace_pending_transactions_if_model_raises_bid_and_increases_gas_price(self, auction_id): # given (model, model_factory) = models(self.keeper, auction_id) amount_to_sell = self.surplus_auction_house.bids(auction_id).amount_to_sell # when simulate_model_output(model=model, price=Wad.from_number(9.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(8.0), gas_price=15) # and self.keeper.check_for_bids() wait_for_other_threads() # then assert round(self.surplus_auction_house.bids(auction_id).bid_amount, 2) == round(Wad(amount_to_sell / Rad.from_number(8.0)), 2) assert self.web3.eth.getBlock('latest', full_transactions=True).transactions[0].gasPrice == 15 # cleanup time_travel_by(self.web3, self.surplus_auction_house.bid_duration() + 1) assert self.surplus_auction_house.settle_auction(auction_id).transact() def test_should_replace_pending_transactions_if_model_lowers_bid_and_increases_gas_price(self, auction_id): # given (model, model_factory) = models(self.keeper, auction_id) amount_to_sell = self.surplus_auction_house.bids(auction_id).amount_to_sell # when simulate_model_output(model=model, price=Wad.from_number(10.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(8.0), gas_price=15) # and self.keeper.check_for_bids() wait_for_other_threads() # then assert self.surplus_auction_house.bids(auction_id).bid_amount == Wad(amount_to_sell * self.geb.oracle_relayer.redemption_price()) / Wad.from_number(8.0) assert self.web3.eth.getBlock('latest', full_transactions=True).transactions[0].gasPrice == 15 # cleanup time_travel_by(self.web3, self.surplus_auction_house.bid_duration() + 1) assert self.surplus_auction_house.settle_auction(auction_id).transact() def test_should_not_bid_on_rounding_errors_with_small_amounts(self, auction_id): # given (model, model_factory) = models(self.keeper, auction_id) amount_to_sell = self.surplus_auction_house.bids(auction_id).amount_to_sell # when price = Wad.from_number(9.0)-Wad(5) simulate_model_output(model=model, price=price) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert self.surplus_auction_house.bids(auction_id).bid_amount == Wad(amount_to_sell * self.geb.oracle_relayer.redemption_price() ) / Wad(price) # when tx_count = self.web3.eth.getTransactionCount(self.keeper_address.address) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert self.web3.eth.getTransactionCount(self.keeper_address.address) == tx_count # cleanup time_travel_by(self.web3, self.surplus_auction_house.bid_duration() + 1) assert self.surplus_auction_house.settle_auction(auction_id).transact() def test_should_settle_when_we_won_the_auction(self, auction_id): # given (model, model_factory) = models(self.keeper, auction_id) # when simulate_model_output(model=model, price=Wad.from_number(8.0)) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then auction = self.surplus_auction_house.bids(auction_id) assert auction.bid_amount > Wad(0) assert round(Wad(auction.amount_to_sell * self.geb.oracle_relayer.redemption_price() ) / auction.bid_amount, 2) == round(Wad.from_number(8.0), 2) system_coin_before = self.geb.safe_engine.coin_balance(self.keeper_address) # when time_travel_by(self.web3, self.surplus_auction_house.bid_duration() + 1) # and self.keeper.check_all_auctions() wait_for_other_threads() system_coin_after = self.geb.safe_engine.coin_balance(self.keeper_address) # then assert system_coin_before < system_coin_after def test_should_not_settle_when_auction_finished_but_somebody_else_won(self, auction_id): # given (model, model_factory) = models(self.keeper, auction_id) amount_to_sell = self.surplus_auction_house.bids(auction_id).amount_to_sell # and assert self.surplus_auction_house.increase_bid_size(auction_id, amount_to_sell, Wad.from_number(16)).transact(from_address=self.other_address) assert self.surplus_auction_house.bids(auction_id).bid_amount == Wad.from_number(16) # when time_travel_by(self.web3, self.surplus_auction_house.bid_duration() + 1) # and self.keeper.check_all_auctions() wait_for_other_threads() # then assert self.surplus_auction_house.bids(auction_id).bid_amount == Wad.from_number(16) def test_should_obey_gas_price_provided_by_the_model(self, auction_id): # given (model, model_factory) = models(self.keeper, auction_id) # when simulate_model_output(model=model, price=Wad.from_number(10.0), gas_price=175000) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert self.web3.eth.getBlock('latest', full_transactions=True).transactions[0].gasPrice == 175000 # cleanup time_travel_by(self.web3, self.surplus_auction_house.bid_duration() + 1) assert self.surplus_auction_house.settle_auction(auction_id).transact() def test_should_use_default_gas_price_if_not_provided_by_the_model(self, auction_id): # given (model, model_factory) = models(self.keeper, auction_id) # when simulate_model_output(model=model, price=Wad.from_number(10.0)) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then auction = self.surplus_auction_house.bids(auction_id) assert auction.high_bidder == self.keeper_address assert auction.bid_amount > Wad(0) assert self.web3.eth.getBlock('latest', full_transactions=True).transactions[0].gasPrice == \ self.default_gas_price print(f"tx gas price is {self.web3.eth.getBlock('latest', full_transactions=True).transactions[0].gasPrice}, web3.eth.gasPrice is {self.web3.eth.gasPrice}") # cleanup time_travel_by(self.web3, self.surplus_auction_house.bid_duration() + 1) assert self.surplus_auction_house.settle_auction(auction_id).transact() def test_should_change_gas_strategy_when_model_output_changes(self, auction_id): # given (model, model_factory) = models(self.keeper, auction_id) amount_to_sell = self.surplus_auction_house.bids(auction_id).amount_to_sell # when first_bid = Wad.from_number(0.0000009) simulate_model_output(model=model, price=first_bid, gas_price=2000) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert self.web3.eth.getBlock('latest', full_transactions=True).transactions[0].gasPrice == 2000 # when second_bid = Wad.from_number(0.0000006) simulate_model_output(model=model, price=second_bid) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert self.surplus_auction_house.bids(auction_id).bid_amount == Wad(amount_to_sell * self.geb.oracle_relayer.redemption_price() / Rad(second_bid)) assert self.web3.eth.getBlock('latest', full_transactions=True).transactions[0].gasPrice == \ self.default_gas_price # when third_bid = Wad.from_number(0.0000003) new_gas_price = int(self.default_gas_price*1.25) simulate_model_output(model=model, price=third_bid, gas_price=new_gas_price) # and self.keeper.check_all_auctions() self.keeper.check_for_bids() wait_for_other_threads() # then assert self.surplus_auction_house.bids(auction_id).bid_amount == Wad(amount_to_sell * self.geb.oracle_relayer.redemption_price() / Rad(third_bid)) assert self.web3.eth.getBlock('latest', full_transactions=True).transactions[0].gasPrice == new_gas_price # cleanup time_travel_by(self.web3, self.surplus_auction_house.bid_duration() + 1) assert self.surplus_auction_house.settle_auction(auction_id).transact() @classmethod def teardown_class(cls): cls.geb = get_geb(get_web3()) #cls.liquidate_safe(web3(), cls.geb, c(cls.geb), auction_income_recipient_address(web3()), our_address(web3())) @classmethod def liquidate_safe(cls, web3, geb, c, auction_income_recipient_address, our_address): safe = geb.safe_engine.safe(c.collateral_type, auction_income_recipient_address) delta_debt = max_delta_debt(geb, c, auction_income_recipient_address) - Wad.from_number(1) assert geb.safe_engine.modify_safe_collateralization(c.collateral_type, auction_income_recipient_address, Wad(0), delta_debt).transact(from_address=auction_income_recipient_address) safe = geb.safe_engine.safe(c.collateral_type, auction_income_recipient_address) set_collateral_price(geb, c, Wad.from_number(10)) # Ensure the SAFE isn't safe assert not is_safe_safe(geb.safe_engine.collateral_type(c.collateral_type.name), safe) # Determine how many liquidations will be required liquidation_quantity = Wad(geb.liquidation_engine.liquidation_quantity(c.collateral_type)) liquidations_required = math.ceil(safe.generated_debt / liquidation_quantity) print(f"locked_collateral={safe.locked_collateral} generated_debt={safe.generated_debt} so {liquidations_required} liquidations are required") c.collateral_auction_house.approve(geb.safe_engine.address, approval_function=approve_safe_modification_directly(from_address=our_address)) # First auction that will be started first_auction_id = c.collateral_auction_house.auctions_started() + 1 # liquidate and bid on each auction for _ in range(liquidations_required): auction_id = liquidate(geb, c, safe) assert auction_id > 0 auction = c.collateral_auction_house.bids(auction_id) bid_amount = Wad(auction.amount_to_raise) + Wad(1) reserve_system_coin(geb, c, our_address, bid_amount) assert c.collateral_auction_house.increase_bid_size(auction_id, auction.amount_to_sell, auction.amount_to_raise).transact(from_address=our_address) time_travel_by(web3, c.collateral_auction_house.total_auction_length()+1) for auction_id in range(first_auction_id, c.collateral_auction_house.auctions_started()+1): assert c.collateral_auction_house.settle_auction(auction_id).transact() set_collateral_price(geb, c, Wad.from_number(200)) safe = geb.safe_engine.safe(c.collateral_type, auction_income_recipient_address)