Exemple #1
0
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()
Exemple #2
0
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()
Exemple #4
0
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)
Exemple #7
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)
Exemple #8
0
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)