Пример #1
0
    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)
Пример #2
0
def kick(web3: Web3, mcd: DssDeployment, gal_address, other_address) -> int:
    joy = mcd.vat.dai(mcd.vow.address)
    woe = (mcd.vat.sin(mcd.vow.address) - mcd.vow.sin()) - mcd.vow.ash()
    print(f'joy={str(joy)[:6]}, woe={str(woe)[:6]}')

    if woe < joy:
        # Bite gal CDP
        c = mcd.collaterals['ETH-B']
        unsafe_cdp = create_unsafe_cdp(mcd, c, Wad.from_number(2), other_address, draw_dai=False)
        flip_kick = bite(mcd, c, unsafe_cdp)

        # Generate some Dai, bid on and win the flip auction without covering all the debt
        reserve_dai(mcd, c, gal_address, Wad.from_number(100), extra_collateral=Wad.from_number(1.1))
        c.flipper.approve(mcd.vat.address, approval_function=hope_directly(from_address=gal_address))
        current_bid = c.flipper.bids(flip_kick)
        bid = Rad.from_number(1.9)
        assert mcd.vat.dai(gal_address) > bid
        assert c.flipper.tend(flip_kick, current_bid.lot, bid).transact(from_address=gal_address)
        time_travel_by(web3, c.flipper.ttl()+1)
        assert c.flipper.deal(flip_kick).transact()

    flog_and_heal(web3, mcd, past_blocks=1200, kiss=False)

    # Kick off the flop auction
    woe = (mcd.vat.sin(mcd.vow.address) - mcd.vow.sin()) - mcd.vow.ash()
    assert mcd.vow.sump() <= woe
    assert mcd.vat.dai(mcd.vow.address) == Rad(0)
    assert mcd.vow.flop().transact(from_address=gal_address)
    return mcd.flopper.kicks()
Пример #3
0
    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()
Пример #4
0
    def test_should_take_partial_if_insufficient_dai_available(self, kick):
        # given
        (model, model_factory) = models(self.keeper, kick)
        (needs_redo, price, initial_lot, initial_tab) = self.clipper.status(kick)
        assert initial_lot == Wad.from_number(1)
        # and we exit all Dai out of the Vat
        assert self.mcd.dai_adapter.exit(self.keeper_address, Wad(self.mcd.vat.dai(self.keeper_address)))\
            .transact(from_address=self.keeper_address)

        # when we have less Dai than we need to cover the auction
        our_price = Ray.from_number(187)
        assert our_price < price
        dai_needed = initial_lot * Wad(our_price)
        half_dai = dai_needed / Wad.from_number(2)
        initial_dai_balance = Wad(self.mcd.vat.dai(self.keeper_address))
        if initial_dai_balance < half_dai:
            print(f"Reserving {half_dai - initial_dai_balance} Dai to get balance of {half_dai}")
            reserve_dai(self.mcd, self.dai_collateral, self.keeper_address, half_dai - initial_dai_balance)
        else:
            print(f"Abandoning {initial_dai_balance - half_dai} Dai to get balance of {half_dai}")
            self.mcd.vat.move(self.keeper_address, self.gal_address, Rad(initial_dai_balance - half_dai))\
                .transact(from_address=keeper_address)
        dai_balance_before_take = Wad(self.mcd.vat.dai(self.keeper_address))
        assert Wad(0) < dai_balance_before_take < dai_needed

        # then ensure we don't bid when the price is too high
        self.simulate_model_bid(model, our_price, reserve_dai_for_bid=False)
        self.keeper.check_all_auctions()
        self.keeper.check_for_bids()
        wait_for_other_threads()
        (needs_redo, price, lot, tab) = self.clipper.status(kick)
        assert lot == initial_lot
        assert tab == initial_tab

        # when we wait for the price to become appropriate
        while lot > Wad(0):
            time_travel_by(self.web3, 1)
            (needs_redo, auction_price, lot, tab) = self.clipper.status(kick)
            if auction_price < our_price:
                break

        # then ensure our bid is submitted using available Dai
        # self.keeper.check_all_auctions()
        self.keeper.check_for_bids()
        wait_for_other_threads()
        (needs_redo, price, lot, tab) = self.clipper.status(kick)
        assert Wad(0) < lot < initial_lot
        our_take = self.last_log()
        assert isinstance(our_take, Clipper.TakeLog)
        assert Wad(self.mcd.vat.dai(self.keeper_address)) < dai_balance_before_take

        # and ensure we don't place a subsequent dusty bid afterward
        self.keeper.check_all_auctions()
        self.keeper.check_for_bids()
        wait_for_other_threads()
        our_take2 = self.last_log()
        assert our_take.tx_hash == our_take2.tx_hash

        # cleanup
        self.take_below_price(kick, price, self.keeper_address)
Пример #5
0
    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
Пример #6
0
    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)
Пример #7
0
    def test_should_increase_gas_price_of_pending_transactions_if_model_increases_gas_price(
            self, mcd, c, kick, keeper):
        # given
        (model, model_factory) = models(keeper, kick)
        flipper = c.flipper

        # when
        bid_price = Wad.from_number(20.0)
        reserve_dai(mcd, c, self.keeper_address, bid_price * tend_lot * 2)
        simulate_model_output(model=model, price=bid_price, gas_price=10)
        # and
        self.start_ignoring_transactions()
        # and
        keeper.check_all_auctions()
        keeper.check_for_bids()
        # and
        simulate_model_output(model=model, price=bid_price, gas_price=15)
        # and
        self.end_ignoring_transactions()
        # and
        keeper.check_for_bids()
        wait_for_other_threads()
        # then
        assert flipper.bids(kick).bid == Rad(bid_price * tend_lot)
        assert self.web3.eth.getBlock(
            'latest', full_transactions=True).transactions[0].gasPrice == 15

        # cleanup
        time_travel_by(self.web3, flipper.ttl() + 1)
        assert flipper.deal(kick).transact()
Пример #8
0
    def eliminate_queued_debt(cls, web3, mcd, keeper_address):
        if mcd.vat.sin(mcd.vow.address) == Rad(0):
            return

        # given the existence of queued debt
        c = mcd.collaterals['ETH-A']
        kick = c.flipper.kicks()
        last_bite = mcd.cat.past_bites(10)[0]

        # when a bid covers the CDP debt
        auction = c.flipper.bids(kick)
        reserve_dai(mcd, c, keeper_address, Wad(auction.tab) + Wad(1))
        c.flipper.approve(
            c.flipper.vat(),
            approval_function=hope_directly(from_address=keeper_address))
        c.approve(keeper_address)
        assert c.flipper.tend(
            kick, auction.lot,
            auction.tab).transact(from_address=keeper_address)
        time_travel_by(web3, c.flipper.ttl() + 1)
        assert c.flipper.deal(kick).transact()

        # when a bid covers the vow debt
        assert mcd.vow.sin_of(last_bite.era(web3)) > Rad(0)
        assert mcd.vow.flog(
            last_bite.era(web3)).transact(from_address=keeper_address)
        assert mcd.vow.heal(mcd.vat.sin(mcd.vow.address)).transact()

        # then ensure queued debt has been auctioned off
        assert mcd.vat.sin(mcd.vow.address) == Rad(0)
Пример #9
0
    def test_should_replace_pending_transactions_if_model_raises_bid_and_increases_gas_price(
            self, kick):
        # given
        (model, model_factory) = models(self.keeper, kick)
        flipper = self.collateral.flipper

        # when
        reserve_dai(self.mcd, self.collateral, self.keeper_address,
                    Wad.from_number(35.0) * tend_lot * 2)
        simulate_model_output(model=model,
                              price=Wad.from_number(15.0),
                              gas_price=10)
        # and
        self.start_ignoring_transactions()
        # and
        self.keeper.check_all_auctions()
        self.keeper.check_for_bids()
        # and
        self.end_ignoring_transactions()
        # and
        simulate_model_output(model=model,
                              price=Wad.from_number(20.0),
                              gas_price=15)
        # and
        self.keeper.check_for_bids()
        wait_for_other_threads()
        # then
        assert flipper.bids(kick).bid == Rad(Wad.from_number(20.0) * tend_lot)
        assert self.web3.eth.getBlock(
            'latest', full_transactions=True).transactions[0].gasPrice == 15

        # cleanup
        time_travel_by(self.web3, flipper.ttl() + 1)
        assert flipper.deal(kick).transact()
Пример #10
0
    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)
Пример #11
0
    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()
Пример #12
0
    def setup_method(self):
        self.web3 = web3()
        self.our_address = our_address(self.web3)
        self.keeper_address = keeper_address(self.web3)
        self.other_address = other_address(self.web3)
        self.gal_address = gal_address(self.web3)
        self.mcd = mcd(self.web3)
        self.flopper = self.mcd.flopper
        self.flopper.approve(
            self.mcd.vat.address,
            approval_function=hope_directly(from_address=self.keeper_address))
        self.flopper.approve(
            self.mcd.vat.address,
            approval_function=hope_directly(from_address=self.other_address))

        self.keeper = AuctionKeeper(args=args(
            f"--eth-from {self.keeper_address} "
            f"--type flop "
            f"--from-block 1 "
            f"--bid-check-interval 0.05 "
            f"--model ./bogus-model.sh"),
                                    web3=self.web3)
        self.keeper.approve()

        reserve_dai(self.mcd, self.mcd.collaterals['ETH-C'],
                    self.keeper_address, Wad.from_number(200.00000))
        reserve_dai(self.mcd, self.mcd.collaterals['ETH-C'],
                    self.other_address, Wad.from_number(200.00000))

        self.sump = self.mcd.vow.sump()  # Rad
Пример #13
0
    def simulate_model_bid(self, model, price: Ray, reserve_dai_for_bid=True):
        assert isinstance(price, Ray)
        assert price > Ray(0)

        assert model.id > 0
        sale = self.clipper.sales(model.id)
        assert sale.lot > Wad(0)

        our_bid = Ray(sale.lot) * price
        if reserve_dai_for_bid:
            reserve_dai(self.mcd, self.dai_collateral, self.keeper_address, Wad(our_bid) + Wad(1))
        simulate_model_output(model=model, price=Wad(price))
Пример #14
0
    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)
Пример #15
0
    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)
Пример #16
0
    def test_should_provide_model_with_updated_info_after_our_own_bid(
            self, mcd, c, gal_address, keeper):
        # given
        flipper = c.flipper
        kick = flipper.kicks()
        (model, model_factory) = models(keeper, kick)

        # when
        keeper.check_all_auctions()
        wait_for_other_threads()
        previous_bid = flipper.bids(model.id)
        # then
        assert model.send_status.call_count == 1

        # when
        initial_bid = flipper.bids(kick)
        our_price = Wad.from_number(30)
        our_bid = our_price * initial_bid.lot
        reserve_dai(mcd, c, self.keeper_address, our_bid)
        simulate_model_output(model=model, price=our_price)
        keeper.check_for_bids()

        # and
        keeper.check_all_auctions()
        wait_for_other_threads()
        # and
        keeper.check_all_auctions()
        wait_for_other_threads()
        # then
        assert model.send_status.call_count > 1
        # and
        status = model.send_status.call_args[0][0]
        assert status.id == kick
        assert status.flipper == flipper.address
        assert status.flapper is None
        assert status.flopper is None
        assert status.bid == Rad(our_price * status.lot)
        assert status.lot == previous_bid.lot
        assert status.tab == previous_bid.tab
        assert status.beg > Wad.from_number(1)
        assert status.guy == self.keeper_address
        assert status.era > 0
        assert status.end > status.era
        assert status.tic > status.era
        assert status.price == our_price

        # cleanup
        time_travel_by(self.web3, flipper.ttl() + 1)
        assert flipper.deal(kick).transact()
Пример #17
0
    def take_with_dai(self, id: int, price: Ray, address: Address):
        assert isinstance(id, int)
        assert isinstance(price, Ray)
        assert isinstance(address, Address)

        lot = self.clipper.sales(id).lot
        assert lot > Wad(0)

        cost = Wad(price * Ray(lot))
        logging.debug(f"reserving {cost} Dai to bid on auction {id}")
        reserve_dai(self.mcd, self.dai_collateral, address, cost)
        assert self.mcd.vat.dai(address) >= Rad(cost)

        logging.debug(f"attempting to take clip {id} at {price}")
        self.clipper.validate_take(id, lot, price, address)
        assert self.clipper.take(id, lot, price, address).transact(from_address=address)

        # confirm that take finished the auction
        (needs_redo, auction_price, lot, tab) = self.clipper.status(id)
        assert not needs_redo
        assert lot == Wad(0) or tab == Rad(0)
Пример #18
0
    def test_should_take_after_someone_else_took(self, kick):
        # given
        (model, model_factory) = models(self.keeper, kick)
        sale = self.clipper.sales(kick)
        assert sale.lot == Wad.from_number(1)

        # when another actor took most of the lot
        time_travel_by(self.web3, 12)
        sale = self.clipper.sales(kick)
        (needs_redo, price, lot, tab) = self.clipper.status(kick)
        their_amt = Wad.from_number(0.6)
        their_bid = Wad(Ray(their_amt) * price)
        assert Rad(their_bid) < sale.tab  # ensure some collateral will be left over
        reserve_dai(self.mcd, self.dai_collateral, self.other_address, their_bid)
        self.clipper.validate_take(kick, their_amt, price, self.other_address)
        assert self.clipper.take(kick, their_amt, price, self.other_address).transact(from_address=self.other_address)
        sale = self.clipper.sales(kick)
        assert sale.lot > Wad(0)

        # and our model is configured to bid a few seconds into the auction
        sale = self.clipper.sales(kick)
        (needs_redo, price, lot, tab) = self.clipper.status(kick)
        assert Rad(price) > sale.tab
        # pad our bid to ensure decimal precision doesn't cause it to be thrown away
        self.simulate_model_bid(model, price + Ray(Wad(1)))
        self.keeper.check_all_auctions()
        self.keeper.check_for_bids()
        wait_for_other_threads()

        # then ensure our take finished the auction
        our_take = self.last_log()
        assert isinstance(our_take, Clipper.TakeLog)
        assert our_take.id == kick
        assert Wad(0) < our_take.lot <= lot
        (needs_redo, price, lot, tab) = self.clipper.status(kick)
        assert not needs_redo
        assert lot == Wad(0) or tab == Rad(0)
Пример #19
0
 def purchase_dai(self, amount: Wad):
     assert isinstance(amount, Wad)
     seller = self.our_address
     reserve_dai(self.mcd, self.mcd.collaterals['ETH-C'], seller, amount)
     assert self.mcd.dai_adapter.exit(seller, amount).transact(from_address=seller)
     assert self.mcd.dai.transfer_from(seller, self.keeper_address, amount).transact(from_address=seller)
Пример #20
0
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import sys

from pymaker.numeric import Wad, Ray, Rad
from tests.conftest import keeper_address, mcd, other_address, reserve_dai, web3

mcd = mcd(web3())
collateral = mcd.collaterals['ETH-C']
keeper_address = keeper_address(web3())
seller = other_address(web3())

amount = Wad.from_number(float(sys.argv[1]))
assert amount > Wad(0)

web3().eth.defaultAccount = seller.address
collateral.approve(seller)
mcd.approve_dai(seller)

reserve_dai(mcd, mcd.collaterals['ETH-C'], seller, amount, Wad.from_number(2))
assert mcd.dai_adapter.exit(seller, amount).transact(from_address=seller)
assert mcd.dai.transfer_from(seller, keeper_address,
                             amount).transact(from_address=seller)
print(
    f'Purchased {str(amount)} Dai, keeper token balance is {str(mcd.dai.balance_of(keeper_address))}'
)