Пример #1
0
def create_risky_cdp(mcd: DssDeployment,
                     c: Collateral,
                     collateral_amount: Wad,
                     gal_address: Address,
                     draw_dai=True) -> Urn:
    assert isinstance(mcd, DssDeployment)
    assert isinstance(c, Collateral)
    assert isinstance(gal_address, Address)

    # Ensure vault isn't already unsafe (if so, this shouldn't be called)
    urn = mcd.vat.urn(c.ilk, gal_address)
    assert is_cdp_safe(mcd.vat.ilk(c.ilk.name), urn)

    # Add collateral to gal vault if necessary
    c.approve(gal_address)
    token = Token(c.ilk.name, c.gem.address, c.adapter.dec())
    print(f"collateral_amount={collateral_amount} ink={urn.ink}")
    dink = collateral_amount - urn.ink
    if dink > Wad(0):
        vat_balance = mcd.vat.gem(c.ilk, gal_address)
        balance = token.normalize_amount(c.gem.balance_of(gal_address))
        print(
            f"before join: dink={dink} vat_balance={vat_balance} balance={balance} vat_gap={dink - vat_balance}"
        )
        if vat_balance < dink:
            vat_gap = dink - vat_balance
            if balance < vat_gap:
                if c.ilk.name.startswith("ETH"):
                    wrap_eth(mcd, gal_address, vat_gap)
                else:
                    raise RuntimeError("Insufficient collateral balance")
            amount_to_join = token.unnormalize_amount(vat_gap)
            if amount_to_join == Wad(
                    0):  # handle dusty balances with non-18-decimal tokens
                amount_to_join += token.unnormalize_amount(token.min_amount)
            assert c.adapter.join(
                gal_address, amount_to_join).transact(from_address=gal_address)
        vat_balance = mcd.vat.gem(c.ilk, gal_address)
        print(
            f"after join: dink={dink} vat_balance={vat_balance} balance={balance} vat_gap={dink - vat_balance}"
        )
        assert vat_balance >= dink
        assert mcd.vat.frob(c.ilk, gal_address, dink,
                            Wad(0)).transact(from_address=gal_address)

    # Put gal CDP at max possible debt
    dart = max_dart(mcd, c, gal_address) - Wad(1)
    if dart > Wad(0):
        print(f"Attempting to frob with dart={dart}")
        assert mcd.vat.frob(c.ilk, gal_address, Wad(0),
                            dart).transact(from_address=gal_address)

    # Draw our Dai, simulating the usual behavior
    urn = mcd.vat.urn(c.ilk, gal_address)
    if draw_dai and urn.art > Wad(0):
        mcd.approve_dai(gal_address)
        assert mcd.dai_adapter.exit(gal_address,
                                    urn.art).transact(from_address=gal_address)
        print(f"Exited {urn.art} Dai from urn")
Пример #2
0
class TestMatchingMarketDecimal:
    def setup_method(self):
        self.web3 = Web3(HTTPProvider("http://localhost:8555"))
        self.web3.eth.defaultAccount = self.web3.eth.accounts[0]
        self.our_address = Address(self.web3.eth.defaultAccount)
        self.token1 = DSToken.deploy(self.web3, 'AAA')
        self.token1_tokenclass = Token('AAA', self.token1.address, 18)
        self.token1.mint(Wad.from_number(10000)).transact()
        self.token2 = DSToken.deploy(self.web3, 'BBB')
        self.token2_tokenclass = Token('BBB', self.token2.address, 6)
        self.token2.mint(Wad.from_number(10000)).transact()

        support_abi = Contract._load_abi(
            __name__, '../pymaker/abi/MakerOtcSupportMethods.abi')
        support_bin = Contract._load_bin(
            __name__, '../pymaker/abi/MakerOtcSupportMethods.bin')
        support_address = Contract._deploy(self.web3, support_abi, support_bin,
                                           [])

        self.otc = MatchingMarket.deploy(self.web3, 2500000000,
                                         support_address)
        self.otc.add_token_pair_whitelist(self.token1.address,
                                          self.token2.address).transact()
        self.otc.approve([self.token1, self.token2], directly())

    def test_get_orders(self):
        buy_amount_order1 = Wad.from_number(5.124988526145090209)
        pay_amount_order1 = Wad.from_number(5.024999999999999500)

        buy_amount_order2 = Wad.from_number(5.102550000000000000)
        pay_amount_order2 = Wad.from_number(5.000000000000000000)

        # given
        self.otc.make(p_token=self.token2_tokenclass,
                      pay_amount=self.token2_tokenclass.unnormalize_amount(
                          pay_amount_order1),
                      b_token=self.token1_tokenclass,
                      buy_amount=buy_amount_order1).transact()

        self.otc.make(p_token=self.token1_tokenclass,
                      pay_amount=pay_amount_order2,
                      b_token=self.token2_tokenclass,
                      buy_amount=self.token2_tokenclass.unnormalize_amount(
                          buy_amount_order2)).transact()

        # then
        assert self.otc.get_orders(
            self.token1_tokenclass,
            self.token2_tokenclass)[0].buy_amount == buy_amount_order2
        assert self.token2_tokenclass.unnormalize_amount(
            self.otc.get_orders(self.token2_tokenclass,
                                self.token1_tokenclass)[0].pay_amount
        ) == self.token2_tokenclass.unnormalize_amount(pay_amount_order1)
Пример #3
0
class TestToken:
    def setup_class(self):
        self.token = Token(
            "COW", Address('0xbeef00000000000000000000000000000000BEEF'), 4)

    def test_convert(self):
        # two
        chain_amount = Wad(20000)
        assert self.token.normalize_amount(chain_amount) == Wad.from_number(2)

        # three
        normalized_amount = Wad.from_number(3)
        assert self.token.unnormalize_amount(normalized_amount) == Wad(30000)

    def test_min_amount(self):
        assert self.token.min_amount == Wad.from_number(0.0001)
        assert float(self.token.min_amount) == 0.0001
        assert self.token.unnormalize_amount(self.token.min_amount) == Wad(1)

        assert Wad.from_number(0.0004) > self.token.min_amount
        assert Wad.from_number(0.00005) < self.token.min_amount

        assert self.token.unnormalize_amount(
            Wad.from_number(0.0006)) > self.token.unnormalize_amount(
                self.token.min_amount)
        assert self.token.unnormalize_amount(
            Wad.from_number(0.00007)) < self.token.unnormalize_amount(
                self.token.min_amount)
        assert self.token.unnormalize_amount(
            Wad.from_number(0.00008)) == Wad(0)
Пример #4
0
    def exit_gem(self):
        if not self.collateral:
            return

        token = Token(self.collateral.ilk.name.split('-')[0], self.collateral.gem.address, self.collateral.adapter.dec())
        vat_balance = self.vat.gem(self.ilk, self.our_address)
        if vat_balance > token.min_amount:
            self.logger.info(f"Exiting {str(vat_balance)} {self.ilk.name} from the Vat")
            assert self.gem_join.exit(self.our_address, token.unnormalize_amount(vat_balance)).transact(gas_price=self.gas_price)
Пример #5
0
    def test_liquidity_given_balance(self):
        """ Test liquidity and mint amount calculations """
        test_token_1 = Token(
            "test_1", Address("0x0000000000000000000000000000000000000001"),
            18)
        test_token_2 = Token(
            "test_2", Address("0x0000000000000000000000000000000000000002"), 6)

        token_1_balance = test_token_1.unnormalize_amount(Wad.from_number(10))
        token_2_balance = test_token_2.unnormalize_amount(Wad.from_number(500))

        sqrt_price_ratio = self.get_starting_sqrt_ratio(
            Wad.from_number(1).value,
            test_token_2.unnormalize_amount(Wad.from_number(3000)).value)
        current_tick = get_tick_at_sqrt_ratio(sqrt_price_ratio)
        ticks = []
        test_pool = Pool(test_token_1, test_token_2, FEES.MEDIUM.value,
                         sqrt_price_ratio, 0, current_tick, ticks)

        tick_lower = current_tick - TICK_SPACING.MEDIUM.value * 5
        tick_upper = current_tick + TICK_SPACING.MEDIUM.value * 7
        rounded_tick_lower = Tick.nearest_usable_tick(
            tick_lower, TICK_SPACING.MEDIUM.value)
        rounded_tick_upper = Tick.nearest_usable_tick(
            tick_upper, TICK_SPACING.MEDIUM.value)
        calculated_position = Position.from_amounts(
            test_pool, rounded_tick_lower, rounded_tick_upper,
            token_1_balance.value, token_2_balance.value, False)

        test_liquidity = calculated_position.liquidity
        assert test_liquidity == 252860870269028

        test_position = Position(test_pool, rounded_tick_lower,
                                 rounded_tick_upper, test_liquidity)

        amount_0, amount_1 = test_position.mint_amounts()
        assert amount_0 == 95107120950731527
        assert amount_1 == 208677042
Пример #6
0
class TestUniswapV3SwapRouter(Contract):
    """ Deployment docs available here: https://github.com/Uniswap/uniswap-v3-periphery/blob/main/deploys.md """

    UniswapV3Factory_abi = Contract._load_abi(__name__, '../pyexchange/abi/UniswapV3Factory.abi')['abi']
    UniswapV3Factory_bin = Contract._load_bin(__name__, '../pyexchange/abi/UniswapV3Factory.bin')
    NFTDescriptor_abi = Contract._load_abi(__name__, '../pyexchange/abi/NFTDescriptor.abi')['abi']
    NFTDescriptor_bin = Contract._load_bin(__name__, '../pyexchange/abi/NFTDescriptor.bin')
    weth_abi = Contract._load_abi(__name__, '../pyexchange/abi/WETH.abi')
    weth_bin = Contract._load_bin(__name__, '../pyexchange/abi/WETH.bin')
    NonfungiblePositionManager_abi = Contract._load_abi(__name__, '../pyexchange/abi/NonfungiblePositionManager.abi')['abi']
    NonfungiblePositionManager_bin = Contract._load_bin(__name__, '../pyexchange/abi/NonfungiblePositionManager.bin')
    SwapRouter_abi = Contract._load_abi(__name__, '../pyexchange/abi/SwapRouter.abi')['abi']
    SwapRouter_bin = Contract._load_bin(__name__, '../pyexchange/abi/SwapRouter.bin')
    UniswapV3TickLens_abi = Contract._load_abi(__name__, '../pyexchange/abi/UniswapV3TickLens.abi')['abi']
    UniswapV3TickLens_bin = Contract._load_bin(__name__, '../pyexchange/abi/UniswapV3TickLens.bin')
    Quoter_abi = Contract._load_abi(__name__, '../pyexchange/abi/Quoter.abi')['abi']
    Quoter_bin = Contract._load_bin(__name__, '../pyexchange/abi/Quoter.bin')

    def setup_class(self):
        self.web3 = Web3(HTTPProvider("http://0.0.0.0:8555", request_kwargs={'timeout': 10}))
        self.web3.eth.defaultAccount = Web3.toChecksumAddress("0x9596C16D7bF9323265C2F2E22f43e6c80eB3d943")
        register_private_key(self.web3, "0x91cf2cc3671a365fcbf38010ff97ee31a5b7e674842663c56769e41600696ead")

        self.our_address = Address(self.web3.eth.defaultAccount)

        # reset ganache EVM state to genesis
        session = requests.Session()
        method = 'evm_revert'
        params = [1]
        payload = {"jsonrpc": "2.0",
                   "method": method,
                   "params": params,
                   "id": 1}
        headers = {'Content-type': 'application/json'}
        response = session.post('http://0.0.0.0:8555', json=payload, headers=headers)
        print("revert response: ", response)

        # constructor args for SwapRouter
        self.factory_address: Address = self._deploy(self.web3, self.UniswapV3Factory_abi, self.UniswapV3Factory_bin, [])
        self.weth_address: Address = self._deploy(self.web3, self.weth_abi, self.weth_bin, [])
        self.token_descriptor_address: Address = self._deploy(self.web3, self.NFTDescriptor_abi, self.NFTDescriptor_bin, [])

        self.swap_router_address = self._deploy(self.web3, self.SwapRouter_abi, self.SwapRouter_bin, [self.factory_address.address, self.weth_address.address])
        self.nonfungiblePositionManager_address = self._deploy(self.web3, self.NonfungiblePositionManager_abi, self.NonfungiblePositionManager_bin, [self.factory_address.address, self.weth_address.address, self.token_descriptor_address.address])
        self.tick_lens_address = self._deploy(self.web3, self.UniswapV3TickLens_abi, self.UniswapV3TickLens_bin, [])
        self.quoter_address = self._deploy(self.web3, self.Quoter_abi, self.Quoter_bin, [self.factory_address.address, self.weth_address.address])

        self.position_manager = PositionManager(self.web3, self.nonfungiblePositionManager_address, self.factory_address, self.tick_lens_address, self.weth_address)
        self.swap_router = SwapRouter(self.web3, self.swap_router_address, self.quoter_address)

        ## Useful for debugging failing transactions
        logger = logging.getLogger('eth')
        logger.setLevel(8)
        # Transact.gas_estimate_for_bad_txs = 210000

        self.ds_dai = DSToken.deploy(self.web3, 'DAI')
        self.ds_usdc = DSToken.deploy(self.web3, 'USDC')
        self.token_dai = Token("DAI", self.ds_dai.address, 18)
        self.token_usdc = Token("USDC", self.ds_usdc.address, 6)
        self.token_weth = Token("WETH", self.weth_address, 18)

        self.position_manager.approve(self.token_dai)
        self.position_manager.approve(self.token_usdc)
        self.position_manager.approve(self.token_weth)
        self.swap_router.approve(self.token_dai)
        self.swap_router.approve(self.token_usdc)
        self.swap_router.approve(self.token_weth)

        # TODO: normalize amounts for decimals
        dai_balance = Wad.from_number(9000000000000000)
        usdc_balance = Wad.from_number(900000000000000)

        self.ds_dai.mint(dai_balance).transact(from_address=self.our_address)
        self.ds_usdc.mint(self.token_usdc.unnormalize_amount(usdc_balance)).transact(from_address=self.our_address)

    def get_starting_sqrt_ratio(self, amount_0, amount_1) -> int:
        return encodeSqrtRatioX96(amount_1, amount_0)

    def deploy_and_mint_weth_dai(self, position_manager_helpers) -> Pool:
        # deploy weth_dai pool and mint initial liquidity to swap against
        position_manager_helper_wethdai = position_manager_helpers(self.web3, self.position_manager,
                                                                   self.NonfungiblePositionManager_abi, self.token_weth,
                                                                   self.token_dai)

        starting_price = self.get_starting_sqrt_ratio(1, 1900)
        weth_dai_pool = position_manager_helper_wethdai.create_and_initialize_pool(
            starting_price, FEES.MEDIUM.value)

        # wrap weth for testing (univ3 only uses weth)
        position_manager_helper_wethdai.wrap_eth(Wad.from_number(25), self.our_address)



        tick_lower = weth_dai_pool.tick_current - TICK_SPACING.MEDIUM.value * 3
        tick_upper = weth_dai_pool.tick_current + TICK_SPACING.MEDIUM.value * 5
        rounded_tick_lower = Tick.nearest_usable_tick(tick_lower, TICK_SPACING.MEDIUM.value)
        rounded_tick_upper = Tick.nearest_usable_tick(tick_upper, TICK_SPACING.MEDIUM.value)

        # TODO: calculate liquidity at a given price, with available balances
        # current liquidity levels result in too small of a balance being minted - need to determine why higher liquidity fails price slippage check
        liquidity_to_mint = 900000000000000

        weth_dai_mint_params = position_manager_helper_wethdai.generate_mint_params(weth_dai_pool,
                                                                                    Position(weth_dai_pool, rounded_tick_lower, rounded_tick_upper,
                                                                                             9000000000000000000), self.our_address,
                                                                                    Fraction(20, 100))
        weth_dai_mint_receipt = self.position_manager.mint(weth_dai_mint_params).transact()
        assert weth_dai_mint_receipt is not None and weth_dai_mint_receipt.successful

        # mint_result = weth_dai_mint_receipt.result[0]
        # print("mint result", mint_result, mint_result.liquidity, mint_result.tick_lower, mint_result.tick_upper)
        token_id = weth_dai_mint_receipt.result[0].token_id
        print("minted_pool token_id", token_id)
        minted_position = self.position_manager.positions(token_id, weth_dai_pool.token_0, weth_dai_pool.token_1)
        print("minted weth_dai value", self.position_manager.price_position(token_id, 1900))
        return minted_position.pool

    def deploy_and_mint_dai_usdc(self, position_manager_helpers) -> Pool:
        # deploy dai_usdc pool and mint initial liquidity to swap against
        position_manager_helper_daiusdc = position_manager_helpers(self.web3, self.position_manager,
                                                                   self.NonfungiblePositionManager_abi, self.token_dai,
                                                                   self.token_usdc)

        dai_usdc_pool = position_manager_helper_daiusdc.create_and_initialize_pool(self.get_starting_sqrt_ratio(1, 1),
                                                                                        FEES.LOW.value)

        liquidity_to_mint = 1000000000
        dai_usdc_mint_params = position_manager_helper_daiusdc.generate_mint_params(dai_usdc_pool,
                                                                                    Position(dai_usdc_pool, -10, 10,
                                                                                             900000000000000000), self.our_address,
                                                                                    Fraction(10, 100))
        dai_usdc_mint_receipt = self.position_manager.mint(dai_usdc_mint_params).transact()
        assert dai_usdc_mint_receipt is not None and dai_usdc_mint_receipt.successful


        token_id = dai_usdc_mint_receipt.result[0].token_id
        print("minted_pool token_id", token_id)
        minted_position = self.position_manager.positions(token_id, dai_usdc_pool.token_0, dai_usdc_pool.token_1)
        print("minted dai_usdc value", self.position_manager.price_position(token_id, 1))
        return minted_position.pool

    def test_encode_route_to_path_multihop_input(self):
        """ Create 3 tokens, and two pools with one shared token between them. Encode the exact input path across these pools."""
        test_token_1 = Token("test_1", Address("0x0000000000000000000000000000000000000001"), 18)
        test_token_2 = Token("test_2", Address("0x0000000000000000000000000000000000000002"), 18)
        test_token_3 = Token("test_3", Address("0x0000000000000000000000000000000000000003"), 18)

        test_pool_1_medium = Pool(test_token_1, test_token_2, 3000, self.get_starting_sqrt_ratio(1, 1), 0, 0, [])
        test_pool_2_low = Pool(test_token_2, test_token_3, 500, self.get_starting_sqrt_ratio(1, 1), 0, 0, [])
        path = [test_pool_1_medium, test_pool_2_low]
        route = Route(path, test_token_1, test_token_3)

        encoded_path = self.swap_router.encode_route_to_path(route, False)
        assert encoded_path == '0x0000000000000000000000000000000000000001000bb800000000000000000000000000000000000000020001f40000000000000000000000000000000000000003'

    def test_encode_route_to_path_multihop_output(self):
        """ Create 3 tokens, and two pools with one shared token between them. Encode the exact output path across these pools."""
        test_token_1 = Token("test_1", Address("0x0000000000000000000000000000000000000001"), 18)
        test_token_2 = Token("test_2", Address("0x0000000000000000000000000000000000000002"), 18)
        test_token_3 = Token("test_3", Address("0x0000000000000000000000000000000000000003"), 18)

        test_pool_1_medium = Pool(test_token_1, test_token_2, 3000, self.get_starting_sqrt_ratio(1, 1), 0, 0, [])
        test_pool_2_low = Pool(test_token_2, test_token_3, 500, self.get_starting_sqrt_ratio(1, 1), 0, 0, [])
        path = [test_pool_1_medium, test_pool_2_low]
        route = Route(path, test_token_1, test_token_3)

        encoded_path = self.swap_router.encode_route_to_path(route, True)
        assert encoded_path == '0x00000000000000000000000000000000000000030001f40000000000000000000000000000000000000002000bb80000000000000000000000000000000000000001'

    def test_should_find_swap_path_across_multiple_pools_exact_input(self, position_manager_helpers):
        # deploy both pools
        weth_dai_pool = self.deploy_and_mint_weth_dai(position_manager_helpers)
        dai_usdc_pool = self.deploy_and_mint_dai_usdc(position_manager_helpers)

        # set trade params
        weth_in = Wad.from_number(.000000000001)
        # weth_in = Wad.from_number(1)
        # weth_in = 1
        print(Wad.from_number(1).value)
        recipient = self.our_address
        slippage_tolerance = Fraction(20, 100)
        deadline = int(time.time() + 1000)

        # define route from weth to usdc via dai
        path = [weth_dai_pool, dai_usdc_pool]
        route = Route(path, self.token_weth, self.token_usdc)

        encoded_path = self.swap_router.encode_route_to_path(route, False)

        trade = Trade.from_route(route, CurrencyAmount.from_raw_amount(self.token_weth, weth_in.value), TRADE_TYPE.EXACT_INPUT.value)
        usdc_out = trade.minimum_amount_out(slippage_tolerance).quotient()
        print("usdc out trade", usdc_out)

        assert usdc_out > 0

        usdc_out_quoter = self.swap_router.quote_exact_input(encoded_path, weth_in)
        print("usdc_out quoter", usdc_out_quoter)

        exact_input_params = ExactInputParams(self.web3, self.SwapRouter_abi, encoded_path, recipient, deadline, weth_in.value, usdc_out)

        swap = self.swap_router.swap_exact_input(exact_input_params).transact()
        assert swap is not None and swap.successful

    def test_should_find_swap_path_across_multiple_pools_exact_output(self, position_manager_helpers):
        # check if pool is already deployed; else retrieve existing pool infromation from the address
        weth_dai_pool_address = self.position_manager.get_pool_address(self.token_weth, self.token_dai, FEES.MEDIUM.value)
        dai_usdc_pool_address = self.position_manager.get_pool_address(self.token_dai, self.token_usdc, FEES.LOW.value)

        if isinstance(weth_dai_pool_address, Address) and weth_dai_pool_address != Address("0x0000000000000000000000000000000000000000"):
            weth_dai_pool = self.position_manager.get_pool(weth_dai_pool_address, self.token_weth, self.token_dai, 1)
        else:
            weth_dai_pool = self.deploy_and_mint_weth_dai(position_manager_helpers)

        if isinstance(dai_usdc_pool_address, Address) and dai_usdc_pool_address != Address("0x0000000000000000000000000000000000000000"):
            dai_usdc_pool = self.position_manager.get_pool(dai_usdc_pool_address, self.token_dai, self.token_usdc, 1)
        else:
            dai_usdc_pool = self.deploy_and_mint_dai_usdc(position_manager_helpers)

        # set trade params
        weth_out = Wad.from_number(.000000000001)
        # weth_out = Wad.from_number(1)
        recipient = self.our_address
        slippage_tolerance = Fraction(20, 100)
        deadline = int(time.time() + 1000)

        # define route from weth to usdc via dai
        path = [weth_dai_pool, dai_usdc_pool]
        route = Route(path, self.token_weth, self.token_usdc)
        encoded_path = self.swap_router.encode_route_to_path(route, True)

        trade = Trade.from_route(route, CurrencyAmount.from_raw_amount(self.token_weth, weth_out.value), TRADE_TYPE.EXACT_INPUT.value)
        usdc_in = trade.minimum_amount_out(slippage_tolerance).quotient()
        print("usdc_in trade: ", usdc_in)

        assert usdc_in > 0

        # usdc_in_quoter = self.swap_router.quote_exact_output(encoded_path, weth_out)
        # print("usdc_in quoter", usdc_in_quoter)

        exact_output_params = ExactOutputParams(self.web3, self.SwapRouter_abi, encoded_path, recipient, deadline, weth_out.value,
                                              usdc_in)

        swap = self.swap_router.swap_exact_output(exact_output_params).transact()
        assert swap is not None and swap.successful

    def test_should_error_when_pools_on_different_networks(self):
        """ test that both pools have matching chain_id value """
        test_token_1 = Token("test_1", Address("0x0000000000000000000000000000000000000001"), 18)
        test_token_2 = Token("test_2", Address("0x0000000000000000000000000000000000000002"), 18)
        test_token_3 = Token("test_3", Address("0x0000000000000000000000000000000000000003"), 18)

        test_pool_1_medium = Pool(test_token_1, test_token_2, 3000, self.get_starting_sqrt_ratio(1, 1), 0, 0, [], 1)
        test_pool_2_low = Pool(test_token_2, test_token_3, 500, self.get_starting_sqrt_ratio(1, 1), 0, 0, [], 2)
        path = [test_pool_1_medium, test_pool_2_low]

        with pytest.raises(Exception):
            route = Route(path, test_token_1, test_token_3)
Пример #7
0
class TestUniswapV2(Contract):
    """
    In order to run automated tests locally, all dependent contracts and deployable bytecode need to be available for deploying contract to local network. 
    Deployable bytecode differs from the runtime bytecode you would see on Etherscan.

    """
    pair_abi = Contract._load_abi(__name__, '../pyexchange/abi/IUniswapV2Pair.abi')
    Irouter_abi = Contract._load_abi(__name__, '../pyexchange/abi/IUniswapV2Router02.abi')['abi']
    router_abi = Contract._load_abi(__name__, '../pyexchange/abi/UniswapV2Router02.abi')
    router_bin = Contract._load_bin(__name__, '../pyexchange/abi/UniswapV2Router02.bin')
    factory_abi = Contract._load_abi(__name__, '../pyexchange/abi/UniswapV2Factory.abi')
    factory_bin = Contract._load_bin(__name__, '../pyexchange/abi/UniswapV2Factory.bin')
    weth_abi = Contract._load_abi(__name__, '../pyexchange/abi/WETH.abi')
    weth_bin = Contract._load_bin(__name__, '../pyexchange/abi/WETH.bin')

    def setup_method(self):

        # Use Ganache docker container
        self.web3 = Web3(HTTPProvider("http://0.0.0.0:8555"))
        self.web3.eth.defaultAccount = Web3.toChecksumAddress("0x9596C16D7bF9323265C2F2E22f43e6c80eB3d943")
        register_private_key(self.web3, "0x91cf2cc3671a365fcbf38010ff97ee31a5b7e674842663c56769e41600696ead")

        self.our_address = Address(self.web3.eth.defaultAccount)

        self.weth_address = self._deploy(self.web3, self.weth_abi, self.weth_bin, [])
        self.factory_address = self._deploy(self.web3, self.factory_abi, self.factory_bin, [self.our_address.address])
        self.router_address = self._deploy(self.web3, self.router_abi, self.router_bin, [self.factory_address.address, self.weth_address.address])
        self._weth_contract = self._get_contract(self.web3, self.weth_abi, self.weth_address)

        self.ds_dai = DSToken.deploy(self.web3, 'DAI')
        self.ds_usdc = DSToken.deploy(self.web3, 'USDC')
        self.token_dai = Token("DAI", self.ds_dai.address, 18)
        self.token_usdc = Token("USDC", self.ds_usdc.address, 6)
        self.token_weth = Token("WETH", self.weth_address, 18)

        self.dai_usdc_uniswap = UniswapV2(self.web3, self.token_dai, self.token_usdc, self.our_address, self.router_address, self.factory_address)
        self.dai_eth_uniswap = UniswapV2(self.web3, self.token_dai, self.token_weth, self.our_address, self.router_address, self.factory_address)
        
        ## Useful for debugging failing transactions
        logger = logging.getLogger('eth')
        logger.setLevel(8)
        # Transact.gas_estimate_for_bad_txs = 210000

    def add_liquidity_tokens(self) -> Receipt:
        self.ds_dai.mint(Wad(17 * 10**18)).transact(from_address=self.our_address)
        self.ds_usdc.mint(self.token_usdc.unnormalize_amount(Wad.from_number(9))).transact(from_address=self.our_address)
        self.dai_usdc_uniswap.approve(self.token_dai)
        self.dai_usdc_uniswap.approve(self.token_usdc)

        add_liquidity_tokens_args = {
            "amount_a_desired": Wad.from_number(1.9),
            "amount_b_desired": self.token_usdc.unnormalize_amount(Wad.from_number(2.0)),
            "amount_a_min": Wad.from_number(1.8),
            "amount_b_min": self.token_usdc.unnormalize_amount(Wad.from_number(1.9))
        }

        return self.dai_usdc_uniswap.add_liquidity(add_liquidity_tokens_args, self.token_dai, self.token_usdc).transact(from_address=self.our_address)

    def add_liquidity_eth(self) -> Receipt:
        self.ds_dai.mint(Wad(300 * 10**18)).transact(from_address=self.our_address)
        self.dai_eth_uniswap.approve(self.token_dai)
        self.dai_eth_uniswap.approve(self.token_weth)

        add_liquidity_eth_args = {
            "amount_b_desired": Wad.from_number(28),
            "amount_a_desired": Wad.from_number(.1),
            "amount_b_min":  Wad.from_number(25),
            "amount_a_min": Wad.from_number(0.01)
        }

        return self.dai_eth_uniswap.add_liquidity_eth(add_liquidity_eth_args, self.token_dai, 0).transact(from_address=self.our_address)

    def test_approval(self):
        # given
        assert self.ds_dai.allowance_of(self.our_address, self.router_address) == Wad(0)

        # when
        self.dai_usdc_uniswap.approve(self.token_dai)

        # then
        assert self.ds_dai.allowance_of(self.our_address, self.router_address) > Wad(0)

    def test_getting_token_balances(self):
        # given
        self.ds_dai.mint(Wad(17 * 10**18)).transact()
        self.ds_usdc.mint(self.token_usdc.unnormalize_amount(Wad.from_number(9))).transact()

        # when
        balance_dai = self.dai_usdc_uniswap.get_account_token_balance(self.token_dai)
        balance_usdc = self.dai_usdc_uniswap.get_account_token_balance(self.token_usdc)

        # then
        assert balance_dai == Wad.from_number(17)
        assert balance_usdc == Wad.from_number(9)

    def test_add_liquidity_tokens(self):
        # when
        add_liquidity = self.add_liquidity_tokens()

        # then
        assert add_liquidity.successful == True

        # when
        self.dai_usdc_uniswap.set_pair_token(self.dai_usdc_uniswap.get_pair_address(self.token_dai.address, self.token_usdc.address))

        # then
        assert self.dai_usdc_uniswap.get_current_liquidity() > Wad.from_number(0)

    def test_add_liquidity_eth(self):
        # when
        add_liquidity_eth = self.add_liquidity_eth()

        # then
        assert add_liquidity_eth.successful == True

        # when
        self.dai_eth_uniswap.set_pair_token(self.dai_eth_uniswap.get_pair_address(self.token_dai.address, self.token_weth.address))

        # then
        assert self.dai_eth_uniswap.get_current_liquidity() > Wad.from_number(0)

    def test_remove_liquidity_tokens(self):
        # given
        add_liquidity = self.add_liquidity_tokens()
        self.dai_usdc_uniswap.set_pair_token(self.dai_usdc_uniswap.get_pair_address(self.token_dai.address, self.token_usdc.address))

        current_liquidity = self.dai_usdc_uniswap.get_current_liquidity()
        total_liquidity = self.dai_usdc_uniswap.get_total_liquidity()
        dai_exchange_balance = self.dai_usdc_uniswap.get_exchange_balance(self.token_dai, self.dai_usdc_uniswap.pair_address)
        usdc_exchange_balance = self.token_usdc.unnormalize_amount(self.dai_usdc_uniswap.get_exchange_balance(self.token_usdc, self.dai_usdc_uniswap.pair_address))

        # then
        assert current_liquidity > Wad.from_number(0)
        assert total_liquidity > Wad.from_number(0)
        assert total_liquidity > current_liquidity

        # given
        amount_a_min = current_liquidity * dai_exchange_balance / total_liquidity
        amount_b_min = current_liquidity * usdc_exchange_balance / total_liquidity
        remove_liquidity_tokens_args = {
            "liquidity": current_liquidity,
            "amountAMin": amount_a_min,
            "amountBMin": amount_b_min
        }

        # when
        remove_liquidity = self.dai_usdc_uniswap.remove_liquidity(remove_liquidity_tokens_args, self.token_dai, self.token_usdc).transact(from_address=self.our_address)

        # then
        assert remove_liquidity.successful == True
        assert self.dai_usdc_uniswap.get_current_liquidity() == Wad.from_number(0)

    def test_remove_liquidity_eth(self):
        # given
        add_liquidity_eth = self.add_liquidity_eth()
        self.dai_eth_uniswap.set_pair_token(self.dai_eth_uniswap.get_pair_address(self.token_dai.address, self.token_weth.address))

        current_liquidity = self.dai_eth_uniswap.get_current_liquidity()
        total_liquidity = self.dai_eth_uniswap.get_total_liquidity()
        dai_exchange_balance = self.dai_eth_uniswap.get_exchange_balance(self.token_dai, self.dai_eth_uniswap.pair_address)
        weth_exchange_balance = self.dai_eth_uniswap.get_exchange_balance(self.token_weth, self.dai_eth_uniswap.pair_address)

        # then
        assert current_liquidity > Wad.from_number(0)
        assert total_liquidity > Wad.from_number(0)
        assert total_liquidity > current_liquidity
        
        # given
        amount_a_min = current_liquidity * weth_exchange_balance / total_liquidity
        amount_b_min = current_liquidity * dai_exchange_balance / total_liquidity
        remove_liquidity_eth_args = {
            "liquidity": current_liquidity,
            "amountBMin": amount_b_min,
            "amountAMin": amount_a_min
        }

        # when
        remove_liquidity = self.dai_eth_uniswap.remove_liquidity_eth(remove_liquidity_eth_args, self.token_dai, 0).transact(from_address=self.our_address)

        # then
        assert remove_liquidity.successful == True
        assert self.dai_eth_uniswap.get_current_liquidity() == Wad.from_number(0)

    def test_tokens_swap(self):
        # given
        add_liquidity = self.add_liquidity_tokens()

        balance_dai_before_swap = self.dai_usdc_uniswap.get_account_token_balance(self.token_dai)
        balance_usdc_before_swap = self.dai_usdc_uniswap.get_account_token_balance(self.token_usdc)

        # when
        swap = self.dai_usdc_uniswap.swap_exact_tokens_for_tokens(Wad.from_number(.4), self.token_usdc.unnormalize_amount(Wad.from_number(.3)), [self.ds_dai.address.address, self.ds_usdc.address.address]).transact(from_address=self.our_address)
        
        # then
        assert swap.successful == True

        balance_dai_after_swap = self.dai_usdc_uniswap.get_account_token_balance(self.token_dai)
        balance_usdc_after_swap = self.dai_usdc_uniswap.get_account_token_balance(self.token_usdc)

        assert balance_dai_after_swap < balance_dai_before_swap
        assert balance_usdc_before_swap < balance_usdc_after_swap
Пример #8
0
class TestUniswapV3PositionManager(Contract):
    """ Deployment docs available here: https://github.com/Uniswap/uniswap-v3-periphery/blob/main/deploys.md """

    UniswapV3Factory_abi = Contract._load_abi(
        __name__, '../pyexchange/abi/UniswapV3Factory.abi')['abi']
    UniswapV3Factory_bin = Contract._load_bin(
        __name__, '../pyexchange/abi/UniswapV3Factory.bin')
    NFTDescriptor_abi = Contract._load_abi(
        __name__, '../pyexchange/abi/NFTDescriptor.abi')['abi']
    NFTDescriptor_bin = Contract._load_bin(
        __name__, '../pyexchange/abi/NFTDescriptor.bin')
    weth_abi = Contract._load_abi(__name__, '../pyexchange/abi/WETH.abi')
    weth_bin = Contract._load_bin(__name__, '../pyexchange/abi/WETH.bin')
    NonfungiblePositionManager_abi = Contract._load_abi(
        __name__, '../pyexchange/abi/NonfungiblePositionManager.abi')['abi']
    NonfungiblePositionManager_bin = Contract._load_bin(
        __name__, '../pyexchange/abi/NonfungiblePositionManager.bin')
    SwapRouter_abi = Contract._load_abi(
        __name__, '../pyexchange/abi/SwapRouter.abi')['abi']
    SwapRouter_bin = Contract._load_bin(__name__,
                                        '../pyexchange/abi/SwapRouter.bin')
    UniswapV3TickLens_abi = Contract._load_abi(
        __name__, '../pyexchange/abi/UniswapV3TickLens.abi')['abi']
    UniswapV3TickLens_bin = Contract._load_bin(
        __name__, '../pyexchange/abi/UniswapV3TickLens.bin')
    Quoter_abi = Contract._load_abi(__name__,
                                    '../pyexchange/abi/Quoter.abi')['abi']
    Quoter_bin = Contract._load_bin(__name__, '../pyexchange/abi/Quoter.bin')

    def setup_class(self):
        # Use Ganache docker container
        self.web3 = Web3(
            HTTPProvider("http://0.0.0.0:8555", request_kwargs={'timeout':
                                                                60}))
        self.web3.eth.defaultAccount = Web3.toChecksumAddress(
            "0x9596C16D7bF9323265C2F2E22f43e6c80eB3d943")
        register_private_key(
            self.web3,
            "0x91cf2cc3671a365fcbf38010ff97ee31a5b7e674842663c56769e41600696ead"
        )

        self.our_address = Address(self.web3.eth.defaultAccount)

        # take snapshot of ganache EVM state at genesis
        session = requests.Session()
        method = 'evm_snapshot'
        params = []
        payload = {
            "jsonrpc": "2.0",
            "method": method,
            "params": params,
            "id": 1
        }
        headers = {'Content-type': 'application/json'}
        response = session.post('http://0.0.0.0:8555',
                                json=payload,
                                headers=headers)

        # constructor args for nonfungiblePositionManager
        self.factory_address: Address = self._deploy(self.web3,
                                                     self.UniswapV3Factory_abi,
                                                     self.UniswapV3Factory_bin,
                                                     [])
        self.weth_address: Address = self._deploy(self.web3, self.weth_abi,
                                                  self.weth_bin, [])
        self.token_descriptor_address: Address = self._deploy(
            self.web3, self.NFTDescriptor_abi, self.NFTDescriptor_bin, [])

        self.nonfungiblePositionManager_address = self._deploy(
            self.web3, self.NonfungiblePositionManager_abi,
            self.NonfungiblePositionManager_bin, [
                self.factory_address.address, self.weth_address.address,
                self.token_descriptor_address.address
            ])

        self.tick_lens_address = self._deploy(self.web3,
                                              self.UniswapV3TickLens_abi,
                                              self.UniswapV3TickLens_bin, [])
        self.position_manager = PositionManager(
            self.web3, self.nonfungiblePositionManager_address,
            self.factory_address, self.tick_lens_address, self.weth_address)

        self.swap_router_address = self._deploy(
            self.web3, self.SwapRouter_abi, self.SwapRouter_bin,
            [self.factory_address.address, self.weth_address.address])
        self.quoter_address = self._deploy(
            self.web3, self.Quoter_abi, self.Quoter_bin,
            [self.factory_address.address, self.weth_address.address])

        self.swap_router = SwapRouter(self.web3, self.swap_router_address,
                                      self.quoter_address)

        ## Useful for debugging failing transactions
        logger = logging.getLogger('eth')
        logger.setLevel(8)
        Transact.gas_estimate_for_bad_txs = 210000

    def setup_method(self):
        self.ds_dai = DSToken.deploy(self.web3, 'DAI')
        self.ds_usdc = DSToken.deploy(self.web3, 'USDC')
        self.token_dai = Token("DAI", self.ds_dai.address, 18)
        self.token_usdc = Token("USDC", self.ds_usdc.address, 6)
        self.token_weth = Token("WETH", self.weth_address, 18)

        self.position_manager.approve(self.token_dai)
        self.position_manager.approve(self.token_usdc)
        self.position_manager.approve(self.token_weth)
        self.swap_router.approve(self.token_dai)
        self.swap_router.approve(self.token_usdc)
        self.swap_router.approve(self.token_weth)

        dai_balance = Wad.from_number(10000000)
        usdc_balance = Wad.from_number(10000000)

        self.ds_dai.mint(dai_balance).transact(from_address=self.our_address)
        self.ds_usdc.mint(
            self.token_usdc.unnormalize_amount(usdc_balance)).transact(
                from_address=self.our_address)

    def create_and_initialize_usdcdai_pool(
            self, starting_sqrt_price_x96: int) -> Pool:

        token_0, token_1 = self.position_manager._set_address_order(
            self.token_dai, self.token_usdc)
        create_pool_receipt = self.position_manager.create_pool(
            token_0, token_1, FEES.LOW.value,
            starting_sqrt_price_x96).transact()

        assert create_pool_receipt is not None and create_pool_receipt.successful
        liquidity = 0  # liquidity is 0 upon initalization
        tick_current = create_pool_receipt.result[0].tick

        pool = Pool(token_0, token_1, FEES.LOW.value, starting_sqrt_price_x96,
                    liquidity, tick_current, [])
        return pool

    def get_starting_sqrt_ratio(self, amount_0, amount_1) -> int:
        return encodeSqrtRatioX96(amount_1, amount_0)

    def generate_mint_usdcdai_params(self, pool: Pool) -> MintParams:

        deadline = int(time.time()) + 1000
        position = Position(pool, -10, 10, 1)
        slippage_tolerance = Fraction(20, 100)
        recipient = self.our_address

        mint_params = MintParams(self.web3,
                                 self.NonfungiblePositionManager_abi, position,
                                 recipient, slippage_tolerance, deadline)
        return mint_params

    def test_liquidity_given_balance(self):
        """ Test liquidity and mint amount calculations """
        test_token_1 = Token(
            "test_1", Address("0x0000000000000000000000000000000000000001"),
            18)
        test_token_2 = Token(
            "test_2", Address("0x0000000000000000000000000000000000000002"), 6)

        token_1_balance = test_token_1.unnormalize_amount(Wad.from_number(10))
        token_2_balance = test_token_2.unnormalize_amount(Wad.from_number(500))

        sqrt_price_ratio = self.get_starting_sqrt_ratio(
            Wad.from_number(1).value,
            test_token_2.unnormalize_amount(Wad.from_number(3000)).value)
        current_tick = get_tick_at_sqrt_ratio(sqrt_price_ratio)
        ticks = []
        test_pool = Pool(test_token_1, test_token_2, FEES.MEDIUM.value,
                         sqrt_price_ratio, 0, current_tick, ticks)

        tick_lower = current_tick - TICK_SPACING.MEDIUM.value * 5
        tick_upper = current_tick + TICK_SPACING.MEDIUM.value * 7
        rounded_tick_lower = Tick.nearest_usable_tick(
            tick_lower, TICK_SPACING.MEDIUM.value)
        rounded_tick_upper = Tick.nearest_usable_tick(
            tick_upper, TICK_SPACING.MEDIUM.value)
        calculated_position = Position.from_amounts(
            test_pool, rounded_tick_lower, rounded_tick_upper,
            token_1_balance.value, token_2_balance.value, False)

        test_liquidity = calculated_position.liquidity
        assert test_liquidity == 252860870269028

        test_position = Position(test_pool, rounded_tick_lower,
                                 rounded_tick_upper, test_liquidity)

        amount_0, amount_1 = test_position.mint_amounts()
        assert amount_0 == 95107120950731527
        assert amount_1 == 208677042

    def test_mint_token_pool_low_price_and_slippage(self):
        """ Test minting a position for a pool that is a small fraction """
        test_token_1 = Token(
            "test_1", Address("0x0000000000000000000000000000000000000001"),
            18)
        test_token_2 = Token(
            "test_2", Address("0x0000000000000000000000000000000000000002"),
            18)

        token_1_balance = Wad.from_number(10)
        token_2_balance = Wad.from_number(100)

        # sqrt_price_ratio = self.get_starting_sqrt_ratio(3000, 1)
        sqrt_price_ratio = self.get_starting_sqrt_ratio(
            Wad.from_number(3000).value,
            Wad.from_number(1).value)
        current_tick = get_tick_at_sqrt_ratio(sqrt_price_ratio)
        ticks = []
        test_pool = Pool(test_token_1, test_token_2, FEES.MEDIUM.value,
                         sqrt_price_ratio, 0, current_tick, ticks)

        # set Position.from_amounts() params
        tick_lower = current_tick - TICK_SPACING.MEDIUM.value * 5
        tick_upper = current_tick + TICK_SPACING.MEDIUM.value * 7
        rounded_tick_lower = Tick.nearest_usable_tick(
            tick_lower, TICK_SPACING.MEDIUM.value)
        rounded_tick_upper = Tick.nearest_usable_tick(
            tick_upper, TICK_SPACING.MEDIUM.value)
        calculated_position = Position.from_amounts(
            test_pool, rounded_tick_lower, rounded_tick_upper,
            token_1_balance.value, token_2_balance.value, False)

        test_liquidity = calculated_position.liquidity

        test_position = Position(test_pool, rounded_tick_lower,
                                 rounded_tick_upper, test_liquidity)

        amount_0, amount_1 = test_position.mint_amounts()

        slippage_tolerance = Fraction(2, 100)
        amount_0_min, amount_1_min = test_position.mint_amounts_with_slippage(
            slippage_tolerance)

        # check that mint amounts will pass periphery contract assertions
        assert amount_0_min > 0 and amount_1_min > 0
        assert amount_0_min < amount_0 and amount_1_min < amount_1

    def test_should_mint_with_nonstandard_decimals(self):
        """ mint a position with one of the tokens having nonstandard decimals.
            Verify that the positions price and minted amounts accounts for decimals.
        """
        test_token_1 = Token(
            "test_1", Address("0x0000000000000000000000000000000000000001"),
            18)
        test_token_2 = Token(
            "test_2", Address("0x0000000000000000000000000000000000000002"), 6)

        # instantiate test pool
        # sqrt_price_ratio = self.get_starting_sqrt_ratio(Wad.from_number(1).value, Wad.from_number(3500).value)
        sqrt_price_ratio = self.get_starting_sqrt_ratio(1, 3500)
        current_tick = get_tick_at_sqrt_ratio(sqrt_price_ratio)
        ticks = []
        test_pool = Pool(test_token_1, test_token_2, FEES.MEDIUM.value,
                         sqrt_price_ratio, 0, current_tick, ticks)

        # based upon current price (expressed in token1/token0), determine the tick to mint the position at
        tick_spacing = TICK_SPACING.MEDIUM.value
        desired_price = PriceFraction(test_token_1, test_token_2, 1, 3500)
        desired_tick = PriceFraction.get_tick_at_price(desired_price)

        # identify upper and lower tick bounds for the position
        desired_lower_tick = Tick.nearest_usable_tick(
            desired_tick - tick_spacing * 5, tick_spacing)
        desired_upper_tick = Tick.nearest_usable_tick(
            desired_tick + tick_spacing * 7, tick_spacing)

        # calculate amount to add for each position.
        ## since test_token_2 has 6 decimals, we must unnormalize the Wad amount from 18 -> 6
        token_1_balance = Wad.from_number(10)
        token_2_balance = Wad.from_number(100)

        token_1_to_add = test_token_1.unnormalize_amount(token_1_balance).value
        token_2_to_add = test_token_2.unnormalize_amount(token_2_balance).value
        # token_1_to_add = token_1_balance.value
        # token_2_to_add = token_2_balance.value

        calculated_position = Position.from_amounts(test_pool,
                                                    desired_lower_tick,
                                                    desired_upper_tick,
                                                    token_1_to_add,
                                                    token_2_to_add, False)

        amount_0, amount_1 = calculated_position.mint_amounts()

        slippage_tolerance = Fraction(2, 100)
        amount_0_min, amount_1_min = calculated_position.mint_amounts_with_slippage(
            slippage_tolerance)

        # check that mint amounts will pass periphery contract assertions
        assert amount_0 > 0 and amount_1 > 0
        assert amount_0_min > 0 and amount_1_min > 0
        assert amount_0_min < amount_0 and amount_1_min < amount_1

        # assume pool.tick_current < desired_upper_tick
        expected_amount_0 = SqrtPriceMath.get_amount_0_delta(
            test_pool.square_root_ratio_x96,
            get_sqrt_ratio_at_tick(desired_upper_tick),
            calculated_position.liquidity, True)
        expected_amount_1 = SqrtPriceMath.get_amount_1_delta(
            get_sqrt_ratio_at_tick(desired_lower_tick),
            test_pool.square_root_ratio_x96, calculated_position.liquidity,
            True)

        assert amount_0 == expected_amount_0
        assert amount_1 == expected_amount_1

        # get amounts from liquidity
        price_lower_tick = pow(1.0001, calculated_position.tick_lower)
        price_upper_tick = pow(1.0001, calculated_position.tick_upper)

        assert price_lower_tick < 3500 < price_upper_tick

        position_token_0 = calculated_position.liquidity / math.sqrt(
            price_upper_tick)
        position_token_1 = calculated_position.liquidity * math.sqrt(
            price_lower_tick)

        # compare original sqrt_price_ratio_x96 to the ratio determined by liquidity to mint
        assert str(sqrt_price_ratio)[:2] == str(
            encodeSqrtRatioX96(int(position_token_1),
                               int(position_token_0)))[:2]
        assert sqrt_price_ratio // Q96 == encodeSqrtRatioX96(
            int(position_token_1), int(position_token_0)) // (2**96)

    def test_mint_token_pool(self, position_manager_helpers):
        """ Integration test to mint a pool with two ERC20 tokens """
        # create pool
        position_manager_helper = position_manager_helpers(
            self.web3, self.position_manager,
            self.NonfungiblePositionManager_abi, self.token_dai,
            self.token_usdc)
        pool = position_manager_helper.create_and_initialize_pool(
            self.get_starting_sqrt_ratio(1, 1), FEES.LOW.value)

        # generate MintParam
        mint_params = position_manager_helper.generate_mint_params(
            pool, Position(pool, -10, 10, 10), self.our_address,
            Fraction(1, 100))

        # mint new position
        gas_price = FixedGasPrice(gas_price=20000000000000000)
        mint_receipt = self.position_manager.mint(mint_params).transact(
            gas_price=gas_price)
        assert mint_receipt is not None and mint_receipt.successful

    def test_mint_eth_token_pool(self, position_manager_helpers):
        """ Integration test to mint a pool where one side is WETH """
        position_manager_helper = position_manager_helpers(
            self.web3, self.position_manager,
            self.NonfungiblePositionManager_abi, self.token_weth,
            self.token_dai)

        # starting pool price for weth-dai 1900
        pool = position_manager_helper.create_and_initialize_pool(
            self.get_starting_sqrt_ratio(1, 1900), FEES.MEDIUM.value)

        # wrap ETH into WETH as UniV3 only works with ERC20 tokens
        position_manager_helper.wrap_eth(Wad.from_number(1), self.our_address)

        mint_params = position_manager_helper.generate_mint_params(
            pool, Position(pool, -60, 60, 10), self.our_address,
            Fraction(20, 100))

        mint_receipt = self.position_manager.mint(mint_params).transact()
        assert mint_receipt is not None and mint_receipt.successful

    def test_get_position_from_id(self):
        # create and intialize pool
        pool = self.create_and_initialize_usdcdai_pool(
            self.get_starting_sqrt_ratio(1, 1))

        mint_params = self.generate_mint_usdcdai_params(pool)

        mint_receipt = self.position_manager.mint(mint_params).transact()
        assert mint_receipt is not None and mint_receipt.successful

        # get the token_id out of the mint transaction receipt
        token_id = mint_receipt.result[0].token_id

        position = self.position_manager.positions(token_id, pool.token_0,
                                                   pool.token_1)

        assert isinstance(position, Position)

        # check that position price matches minted position expectations
        position_price = self.position_manager.price_position(token_id, 1)
        assert position_price == Wad(4002000400040001800000)

    # TODO: implement permit
    # https://github.com/Uniswap/uniswap-v3-periphery/blob/main/contracts/base/Multicall.sol
    # def test_multicall_permit_mint(self):
    #     amount_0 = 100 * 10 ** 6
    #     amount_1 = 100 * 10 ** 18
    #     # create and intialize pool
    #     pool = self.create_and_initialize_pool(self.get_starting_sqrt_ratio(amount_0, amount_1))
    #     # # create and intialize pool
    #
    #     multicall_mint_params = self.generate_mint_params(pool)
    #     multicall_mint_receipt = self.position_manager.multicall([multicall_mint_params.calldata.as_bytes()]).transact()
    #     assert multicall_mint_receipt is not None
    #
    #     # check token balance
    #     assert self.position_manager.balance_of()

    def test_burn(self):
        """ Integration test of minting a new position, removing liquidity from it, then burning the position """
        # create and intialize pool
        pool = self.create_and_initialize_usdcdai_pool(
            self.get_starting_sqrt_ratio(1, 1))

        # mint new position
        mint_params = self.generate_mint_usdcdai_params(pool)
        mint_receipt = self.position_manager.mint(mint_params).transact()

        # get the token_id out of the mint transaction receipt
        token_id = mint_receipt.result[0].token_id
        liquidity = mint_receipt.result[0].liquidity
        amount_0 = mint_receipt.result[0].amount_0
        amount_1 = mint_receipt.result[0].amount_1

        # decrease liquidity - remove all minted liquidity
        decrease_liquidity_params = DecreaseLiquidityParams(
            self.web3, self.NonfungiblePositionManager_abi, token_id,
            liquidity, amount_0 - 1, amount_1 - 1, None)
        decrease_liquidity_receipt = self.position_manager.decrease_liquidity(
            decrease_liquidity_params).transact()

        assert decrease_liquidity_receipt is not None and decrease_liquidity_receipt.successful

        # burn previously created position
        burn_params = BurnParams(self.web3,
                                 self.NonfungiblePositionManager_abi, token_id)
        burn_receipt = self.position_manager.burn(burn_params).transact()

        assert burn_receipt is not None and burn_receipt.successful

    # multicall(decreaseLiquidity, burn)
    def test_multicall_burn(self):
        """ Integration test of minting a new position, removing liquidity from it, then burning the position.
            All in a single multicall transaction
        """
        # create and intialize pool
        pool = self.create_and_initialize_usdcdai_pool(
            self.get_starting_sqrt_ratio(1, 1))

        # mint new position
        mint_params = self.generate_mint_usdcdai_params(pool)
        mint_receipt = self.position_manager.mint(mint_params).transact()

        # get the token_id out of the mint transaction receipt
        token_id = mint_receipt.result[0].token_id
        liquidity = mint_receipt.result[0].liquidity
        amount_0 = mint_receipt.result[0].amount_0
        amount_1 = mint_receipt.result[0].amount_1

        # decrease liquidity - remove all minted liquidity
        decrease_liquidity_params = DecreaseLiquidityParams(
            self.web3, self.NonfungiblePositionManager_abi, token_id,
            liquidity, amount_0 - 1, amount_1 - 1, None)

        # burn position following liquidity removal
        burn_params = BurnParams(self.web3,
                                 self.NonfungiblePositionManager_abi, token_id)

        # collect encoded params into a list
        multicall_calldata = [
            decrease_liquidity_params.calldata.value,
            burn_params.calldata.value
        ]

        multicall_params = MulticallParams(self.web3,
                                           self.NonfungiblePositionManager_abi,
                                           multicall_calldata).calldata.value
        multicall_receipt = self.position_manager.multicall([multicall_params
                                                             ]).transact()
        assert multicall_receipt is not None and multicall_receipt.successful

    def test_collect_exact_output_swap(self, position_manager_helpers):
        """ Integration test of minting a new position, executing an ExactOutput swap against the positions liquidity
            to ensure fees are available, and then collecting those fees.
        """
        # create and intialize pool
        position_manager_helper = position_manager_helpers(
            self.web3, self.position_manager,
            self.NonfungiblePositionManager_abi, self.token_dai,
            self.token_usdc)
        pool = position_manager_helper.create_and_initialize_pool(
            self.get_starting_sqrt_ratio(1, 1), FEES.LOW.value)

        # mint initial liquidity
        mint_params = position_manager_helper.generate_mint_params(
            pool, Position(pool, -10, 10, 100000000000000), self.our_address,
            Fraction(20, 100))
        mint_receipt = self.position_manager.mint(mint_params).transact()

        # get the token_id out of the mint transaction receipt
        token_id = mint_receipt.result[0].token_id
        minted_position = self.position_manager.positions(
            token_id, pool.token_0, pool.token_1)

        # execute swaps against the pool to generate fees
        amount_out = Wad.from_number(10)
        slippage_tolerance = Fraction(20, 100)  # equivalent to 0.2
        recipient = self.our_address
        deadline = int(time.time() + 10000)

        # Build Route and Trade entities that can be used to determine amount_in
        route = Route([minted_position.pool], minted_position.pool.token_0,
                      minted_position.pool.token_1)
        trade = Trade.from_route(
            route,
            CurrencyAmount.from_raw_amount(minted_position.pool.token_1,
                                           amount_out.value),
            TRADE_TYPE.EXACT_OUTPUT_SINGLE.value)

        max_amount_in = trade.maximum_amount_in(slippage_tolerance).quotient()
        trade_amount_in = trade.input_amount.quotient()
        print("alts", max_amount_in, trade_amount_in)
        sqrt_price_limit = 100000000000000000000000
        amount_in = self.swap_router.quote_exact_output_single(
            pool.token_0, pool.token_1, pool.fee, amount_out.value,
            sqrt_price_limit)

        # amount_in = trade.input_amount.quotient()

        # Instantiate ExactOutputSingleParams that will be used to generate fees
        exact_output_single_params = ExactOutputSingleParams(
            self.web3, self.SwapRouter_abi, trade.route.token_path[0],
            trade.route.token_path[1], trade.route.pools[0].fee, recipient,
            deadline, amount_out.value, amount_in, sqrt_price_limit)
        swap = self.swap_router.swap_exact_output_single(
            exact_output_single_params).transact()
        assert swap is not None and swap.successful

        position_amount_0, position_amount_1 = self.position_manager.get_position_reserves(
            token_id)

        # collect fees from position
        collect_params = CollectParams(self.web3,
                                       self.NonfungiblePositionManager_abi,
                                       token_id, self.our_address,
                                       int(position_amount_0),
                                       int(position_amount_1))
        collect_receipt = self.position_manager.collect(
            collect_params).transact()

        assert collect_receipt is not None and collect_receipt.successful

    def test_collect_exact_input_swap(self):
        """ Integration test of minting a new position, executing an ExactInput swap against the positions liquidity
            to ensure fees are available, and then collecting those fees.
        """
        # create and intialize pool
        pool = self.create_and_initialize_usdcdai_pool(
            self.get_starting_sqrt_ratio(1, 1))

        # mint new position
        mint_params = self.generate_mint_usdcdai_params(pool)
        mint_receipt = self.position_manager.mint(mint_params).transact()

        # get the token_id out of the mint transaction receipt
        token_id = mint_receipt.result[0].token_id

        # instantiate a Position object from the token_id
        minted_position = self.position_manager.positions(
            token_id, pool.token_0, pool.token_1)

        amount_in = 1
        recipient = self.our_address
        deadline = int(time.time() + 10000)
        price_limit = 0

        # Build Route and Trade entities that can be used to determine amount_out
        route = Route([minted_position.pool], minted_position.pool.token_0,
                      minted_position.pool.token_1)
        trade = Trade.from_route(
            route,
            CurrencyAmount.from_raw_amount(minted_position.pool.token_0,
                                           amount_in),
            TRADE_TYPE.EXACT_INPUT_SINGLE.value)

        # Instantiate ExactInputSingleParams that will be used to generate fees
        amount_out = trade.output_amount.quotient()
        exact_input_single_params = ExactInputSingleParams(
            self.web3, self.SwapRouter_abi, trade.route.token_path[0],
            trade.route.token_path[1], trade.route.pools[0].fee, recipient,
            deadline, amount_in, amount_out, price_limit)

        swap = self.swap_router.swap_exact_input_single(
            exact_input_single_params).transact()
        assert swap is not None and swap.successful

        # collect fees from position
        collect_params = CollectParams(self.web3,
                                       self.NonfungiblePositionManager_abi,
                                       token_id, self.our_address, MAX_UINT128,
                                       MAX_UINT128)
        collect_receipt = self.position_manager.collect(
            collect_params).transact()

        assert collect_receipt is not None and collect_receipt.successful
Пример #9
0
    def test_should_mint_with_nonstandard_decimals(self):
        """ mint a position with one of the tokens having nonstandard decimals.
            Verify that the positions price and minted amounts accounts for decimals.
        """
        test_token_1 = Token(
            "test_1", Address("0x0000000000000000000000000000000000000001"),
            18)
        test_token_2 = Token(
            "test_2", Address("0x0000000000000000000000000000000000000002"), 6)

        # instantiate test pool
        # sqrt_price_ratio = self.get_starting_sqrt_ratio(Wad.from_number(1).value, Wad.from_number(3500).value)
        sqrt_price_ratio = self.get_starting_sqrt_ratio(1, 3500)
        current_tick = get_tick_at_sqrt_ratio(sqrt_price_ratio)
        ticks = []
        test_pool = Pool(test_token_1, test_token_2, FEES.MEDIUM.value,
                         sqrt_price_ratio, 0, current_tick, ticks)

        # based upon current price (expressed in token1/token0), determine the tick to mint the position at
        tick_spacing = TICK_SPACING.MEDIUM.value
        desired_price = PriceFraction(test_token_1, test_token_2, 1, 3500)
        desired_tick = PriceFraction.get_tick_at_price(desired_price)

        # identify upper and lower tick bounds for the position
        desired_lower_tick = Tick.nearest_usable_tick(
            desired_tick - tick_spacing * 5, tick_spacing)
        desired_upper_tick = Tick.nearest_usable_tick(
            desired_tick + tick_spacing * 7, tick_spacing)

        # calculate amount to add for each position.
        ## since test_token_2 has 6 decimals, we must unnormalize the Wad amount from 18 -> 6
        token_1_balance = Wad.from_number(10)
        token_2_balance = Wad.from_number(100)

        token_1_to_add = test_token_1.unnormalize_amount(token_1_balance).value
        token_2_to_add = test_token_2.unnormalize_amount(token_2_balance).value
        # token_1_to_add = token_1_balance.value
        # token_2_to_add = token_2_balance.value

        calculated_position = Position.from_amounts(test_pool,
                                                    desired_lower_tick,
                                                    desired_upper_tick,
                                                    token_1_to_add,
                                                    token_2_to_add, False)

        amount_0, amount_1 = calculated_position.mint_amounts()

        slippage_tolerance = Fraction(2, 100)
        amount_0_min, amount_1_min = calculated_position.mint_amounts_with_slippage(
            slippage_tolerance)

        # check that mint amounts will pass periphery contract assertions
        assert amount_0 > 0 and amount_1 > 0
        assert amount_0_min > 0 and amount_1_min > 0
        assert amount_0_min < amount_0 and amount_1_min < amount_1

        # assume pool.tick_current < desired_upper_tick
        expected_amount_0 = SqrtPriceMath.get_amount_0_delta(
            test_pool.square_root_ratio_x96,
            get_sqrt_ratio_at_tick(desired_upper_tick),
            calculated_position.liquidity, True)
        expected_amount_1 = SqrtPriceMath.get_amount_1_delta(
            get_sqrt_ratio_at_tick(desired_lower_tick),
            test_pool.square_root_ratio_x96, calculated_position.liquidity,
            True)

        assert amount_0 == expected_amount_0
        assert amount_1 == expected_amount_1

        # get amounts from liquidity
        price_lower_tick = pow(1.0001, calculated_position.tick_lower)
        price_upper_tick = pow(1.0001, calculated_position.tick_upper)

        assert price_lower_tick < 3500 < price_upper_tick

        position_token_0 = calculated_position.liquidity / math.sqrt(
            price_upper_tick)
        position_token_1 = calculated_position.liquidity * math.sqrt(
            price_lower_tick)

        # compare original sqrt_price_ratio_x96 to the ratio determined by liquidity to mint
        assert str(sqrt_price_ratio)[:2] == str(
            encodeSqrtRatioX96(int(position_token_1),
                               int(position_token_0)))[:2]
        assert sqrt_price_ratio // Q96 == encodeSqrtRatioX96(
            int(position_token_1), int(position_token_0)) // (2**96)
Пример #10
0
class OasisMarketMakerKeeper:
    """Keeper acting as a market maker on OasisDEX."""

    logger = logging.getLogger()

    def __init__(self, args: list, **kwargs):
        parser = argparse.ArgumentParser(prog='oasis-market-maker-keeper')

        parser.add_argument(
            "--endpoint-uri",
            type=str,
            help="JSON-RPC uri (example: `http://localhost:8545`)")

        parser.add_argument(
            "--rpc-host",
            default="localhost",
            type=str,
            help="[DEPRECATED] JSON-RPC host (default: `localhost')")

        parser.add_argument(
            "--rpc-port",
            default=8545,
            type=int,
            help="[DEPRECATED] JSON-RPC port (default: `8545')")

        parser.add_argument("--rpc-timeout",
                            type=int,
                            default=10,
                            help="JSON-RPC timeout (in seconds, default: 10)")

        parser.add_argument(
            "--eth-from",
            type=str,
            required=True,
            help="Ethereum account from which to send transactions")

        parser.add_argument(
            "--eth-key",
            type=str,
            nargs='*',
            help=
            "Ethereum private key(s) to use (e.g. 'key_file=aaa.json,pass_file=aaa.pass')"
        )

        parser.add_argument("--tub-address",
                            type=str,
                            required=False,
                            help="Ethereum address of the Tub contract")

        parser.add_argument("--oasis-address",
                            type=str,
                            required=True,
                            help="Ethereum address of the OasisDEX contract")

        parser.add_argument(
            "--oasis-support-address",
            type=str,
            required=False,
            help="Ethereum address of the OasisDEX support contract")

        parser.add_argument("--buy-token-address",
                            type=str,
                            required=True,
                            help="Ethereum address of the buy token")

        parser.add_argument("--sell-token-address",
                            type=str,
                            required=True,
                            help="Ethereum address of the sell token")

        parser.add_argument("--buy-token-name",
                            type=str,
                            required=True,
                            help="Ethereum address of the buy token")

        parser.add_argument("--sell-token-name",
                            type=str,
                            required=True,
                            help="Ethereum address of the sell token")

        parser.add_argument("--buy-token-decimals",
                            type=int,
                            required=True,
                            help="Ethereum address of the buy token")

        parser.add_argument("--sell-token-decimals",
                            type=int,
                            required=True,
                            help="Ethereum address of the sell token")

        parser.add_argument("--config",
                            type=str,
                            required=True,
                            help="Bands configuration file")

        parser.add_argument("--price-feed",
                            type=str,
                            required=True,
                            help="Source of price feed")

        parser.add_argument(
            "--price-feed-expiry",
            type=int,
            default=120,
            help="Maximum age of the price feed (in seconds, default: 120)")

        parser.add_argument("--spread-feed",
                            type=str,
                            help="Source of spread feed")

        parser.add_argument(
            "--spread-feed-expiry",
            type=int,
            default=3600,
            help="Maximum age of the spread feed (in seconds, default: 3600)")

        parser.add_argument("--control-feed",
                            type=str,
                            help="Source of control feed")

        parser.add_argument(
            "--control-feed-expiry",
            type=int,
            default=86400,
            help="Maximum age of the control feed (in seconds, default: 86400)"
        )

        parser.add_argument("--order-history",
                            type=str,
                            help="Endpoint to report active orders to")

        parser.add_argument(
            "--order-history-every",
            type=int,
            default=30,
            help=
            "Frequency of reporting active orders (in seconds, default: 30)")

        parser.add_argument(
            "--round-places",
            type=int,
            default=2,
            help="Number of decimal places to round order prices to (default=2)"
        )

        parser.add_argument(
            "--min-eth-balance",
            type=float,
            default=0,
            help="Minimum ETH balance below which keeper will cease operation")

        parser.add_argument("--gas-price",
                            type=int,
                            default=0,
                            help="Gas price (in Wei)")

        parser.add_argument(
            "--smart-gas-price",
            dest='smart_gas_price',
            action='store_true',
            help=
            "Use smart gas pricing strategy, based on the ethgasstation.info feed"
        )

        parser.add_argument("--ethgasstation-api-key",
                            type=str,
                            default=None,
                            help="ethgasstation API key")

        parser.add_argument(
            "--refresh-frequency",
            type=int,
            default=10,
            help="Order book refresh frequency (in seconds, default: 10)")

        parser.add_argument("--debug",
                            dest='debug',
                            action='store_true',
                            help="Enable debug output")

        self.arguments = parser.parse_args(args)
        setup_logging(self.arguments)

        if 'web3' in kwargs:
            self.web3 = kwargs['web3']
        elif self.arguments.endpoint_uri:
            self.web3: Web3 = web3_via_http(self.arguments.endpoint_uri,
                                            self.arguments.rpc_timeout)
        else:
            self.logger.warning(
                "Configuring node endpoint by host and port is deprecated; please use --endpoint-uri"
            )
            self.web3 = Web3(
                HTTPProvider(
                    endpoint_uri=
                    f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}",
                    request_kwargs={"timeout": self.arguments.rpc_timeout}))

        self.web3.eth.defaultAccount = self.arguments.eth_from
        register_keys(self.web3, self.arguments.eth_key)
        self.our_address = Address(self.arguments.eth_from)
        self.otc = MatchingMarket(
            web3=self.web3,
            address=Address(self.arguments.oasis_address),
            support_address=Address(self.arguments.oasis_support_address)
            if self.arguments.oasis_support_address else None)

        tub = Tub(web3=self.web3, address=Address(self.arguments.tub_address)) \
            if self.arguments.tub_address is not None else None

        self.token_buy = ERC20Token(web3=self.web3,
                                    address=Address(
                                        self.arguments.buy_token_address))
        self.token_sell = ERC20Token(web3=self.web3,
                                     address=Address(
                                         self.arguments.sell_token_address))
        self.buy_token = Token(name=self.arguments.buy_token_name,
                               address=Address(
                                   self.arguments.buy_token_address),
                               decimals=self.arguments.buy_token_decimals)
        self.sell_token = Token(name=self.arguments.sell_token_name,
                                address=Address(
                                    self.arguments.sell_token_address),
                                decimals=self.arguments.sell_token_decimals)
        self.min_eth_balance = Wad.from_number(self.arguments.min_eth_balance)
        self.bands_config = ReloadableConfig(self.arguments.config)
        self.gas_price = GasPriceFactory().create_gas_price(
            self.web3, self.arguments)
        self.price_feed = PriceFeedFactory().create_price_feed(
            self.arguments, tub)
        self.spread_feed = create_spread_feed(self.arguments)
        self.control_feed = create_control_feed(self.arguments)
        self.order_history_reporter = create_order_history_reporter(
            self.arguments)

        self.history = History()
        self.order_book_manager = OrderBookManager(
            refresh_frequency=self.arguments.refresh_frequency)
        self.order_book_manager.get_orders_with(lambda: self.our_orders())
        self.order_book_manager.place_orders_with(self.place_order_function)
        self.order_book_manager.cancel_orders_with(self.cancel_order_function)
        self.order_book_manager.enable_history_reporting(
            self.order_history_reporter, self.our_buy_orders,
            self.our_sell_orders)
        self.order_book_manager.start()

    def main(self):
        with Lifecycle(self.web3) as lifecycle:
            lifecycle.initial_delay(10)
            lifecycle.on_startup(self.startup)
            lifecycle.every(1, self.synchronize_orders)
            lifecycle.on_shutdown(self.shutdown)

    def startup(self):
        self.approve()

    def shutdown(self):
        self.order_book_manager.cancel_all_orders(final_wait_time=60)

    def approve(self):
        """Approve OasisDEX to access our balances, so we can place orders."""
        self.otc.approve([self.token_sell, self.token_buy],
                         directly(gas_price=self.gas_price))

    def our_available_balance(self, token: ERC20Token) -> Wad:
        if token.symbol() == self.buy_token.name:
            return self.buy_token.normalize_amount(
                token.balance_of(self.our_address))
        else:
            return self.sell_token.normalize_amount(
                token.balance_of(self.our_address))

    def our_orders(self):
        return list(
            filter(
                lambda order: order.maker == self.our_address,
                self.otc.get_orders(self.sell_token, self.buy_token) +
                self.otc.get_orders(self.buy_token, self.sell_token)))

    def our_sell_orders(self, our_orders: list):
        return list(
            filter(
                lambda order: order.buy_token == self.token_buy.address and
                order.pay_token == self.token_sell.address, our_orders))

    def our_buy_orders(self, our_orders: list):
        return list(
            filter(
                lambda order: order.buy_token == self.token_sell.address and
                order.pay_token == self.token_buy.address, our_orders))

    def synchronize_orders(self):
        # If keeper balance is below `--min-eth-balance`, cancel all orders but do not terminate
        # the keeper, keep processing blocks as the moment the keeper gets a top-up it should
        # resume activity straight away, without the need to restart it.
        if eth_balance(self.web3, self.our_address) < self.min_eth_balance:
            self.logger.warning(
                "Keeper ETH balance below minimum. Cancelling all orders.")
            self.order_book_manager.cancel_all_orders()
            return

        bands = Bands.read(self.bands_config, self.spread_feed,
                           self.control_feed, self.history)
        order_book = self.order_book_manager.get_order_book()
        target_price = self.price_feed.get_price()
        # Cancel orders
        cancellable_orders = bands.cancellable_orders(
            our_buy_orders=self.our_buy_orders(order_book.orders),
            our_sell_orders=self.our_sell_orders(order_book.orders),
            target_price=target_price)
        if len(cancellable_orders) > 0:
            self.order_book_manager.cancel_orders(cancellable_orders)
            return

        # Do not place new orders if other new orders are being placed. In contrary to other keepers,
        # we allow placing new orders when other orders are being cancelled. This is because Ethereum
        # transactions are ordered so we are sure that the order placement will not 'overtake'
        # order cancellation.
        if order_book.orders_being_placed:
            self.logger.debug(
                "Other orders are being placed, not placing new orders")
            return

        # Place new orders
        self.order_book_manager.place_orders(
            bands.new_orders(
                our_buy_orders=self.our_buy_orders(order_book.orders),
                our_sell_orders=self.our_sell_orders(order_book.orders),
                our_buy_balance=self.our_available_balance(self.token_buy),
                our_sell_balance=self.our_available_balance(self.token_sell),
                target_price=target_price)[0])

    def place_order_function(self, new_order: NewOrder):
        assert (isinstance(new_order, NewOrder))

        if new_order.is_sell:
            buy_or_sell = "SELL"
            pay_token = self.token_sell.address
            buy_token = self.token_buy.address
            new_order.buy_amount = self.buy_token.unnormalize_amount(
                new_order.buy_amount)
            b_token = self.buy_token
            p_token = self.sell_token
            new_order.pay_amount = self.sell_token.unnormalize_amount(
                new_order.pay_amount)
            token_name = self.sell_token.name
            quote_token = self.buy_token.name

        else:
            buy_or_sell = "BUY"
            pay_token = self.token_buy.address
            buy_token = self.token_sell.address
            new_order.pay_amount = self.buy_token.unnormalize_amount(
                new_order.pay_amount)
            p_token = self.buy_token
            b_token = self.sell_token
            new_order.buy_amount = self.sell_token.unnormalize_amount(
                new_order.buy_amount)
            token_name = self.sell_token.name
            quote_token = self.buy_token.name

        transact = self.otc.make(
            p_token=p_token,
            pay_amount=new_order.pay_amount,
            b_token=b_token,
            buy_amount=new_order.buy_amount).transact(gas_price=self.gas_price)

        if new_order.is_sell:
            new_order.buy_amount = self.buy_token.normalize_amount(
                new_order.buy_amount)
            new_order.pay_amount = self.sell_token.normalize_amount(
                new_order.pay_amount)
            buy_or_sell_price = new_order.buy_amount / new_order.pay_amount
            amount = new_order.pay_amount

        else:
            new_order.pay_amount = self.buy_token.normalize_amount(
                new_order.pay_amount)
            new_order.buy_amount = self.sell_token.normalize_amount(
                new_order.buy_amount)
            buy_or_sell_price = new_order.pay_amount / new_order.buy_amount
            amount = new_order.buy_amount

        if transact is not None and transact.successful and transact.result is not None:
            self.logger.info(
                f'Placing {buy_or_sell} order of amount {amount} {token_name} @ price {buy_or_sell_price} {quote_token}'
            )
            self.logger.info(
                f'Placing {buy_or_sell} order pay token: {p_token.name} with amount: {new_order.pay_amount}, buy token: {b_token.name} with amount: {new_order.buy_amount}'
            )
            return Order(market=self.otc,
                         order_id=transact.result,
                         maker=self.our_address,
                         pay_token=pay_token,
                         pay_amount=new_order.pay_amount,
                         buy_token=buy_token,
                         buy_amount=new_order.buy_amount,
                         timestamp=0)
        else:
            return None

    def cancel_order_function(self, order):
        transact = self.otc.kill(
            order.order_id).transact(gas_price=self.gas_price)
        return transact is not None and transact.successful
Пример #11
0
class TestUniswapV2MarketMakerKeeper:

    Irouter_abi = Contract._load_abi(
        __name__,
        '../lib/pyexchange/pyexchange/abi/IUniswapV2Router02.abi')['abi']
    router_abi = Contract._load_abi(
        __name__, '../lib/pyexchange/pyexchange/abi/UniswapV2Router02.abi')
    router_bin = Contract._load_bin(
        __name__, '../lib/pyexchange/pyexchange/abi/UniswapV2Router02.bin')
    factory_abi = Contract._load_abi(
        __name__, '../lib/pyexchange/pyexchange/abi/UniswapV2Factory.abi')
    factory_bin = Contract._load_bin(
        __name__, '../lib/pyexchange/pyexchange/abi/UniswapV2Factory.bin')
    weth_abi = Contract._load_abi(__name__,
                                  '../lib/pyexchange/pyexchange/abi/WETH.abi')
    weth_bin = Contract._load_bin(__name__,
                                  '../lib/pyexchange/pyexchange/abi/WETH.bin')

    logger = logging.getLogger()

    def setup_method(self):

        # Use Ganache docker container
        self.web3 = Web3(HTTPProvider("http://0.0.0.0:8555"))
        self.web3.eth.defaultAccount = Web3.toChecksumAddress(
            "0x9596C16D7bF9323265C2F2E22f43e6c80eB3d943")
        self.our_address = Address(self.web3.eth.defaultAccount)

        self.private_key = "0x91cf2cc3671a365fcbf38010ff97ee31a5b7e674842663c56769e41600696ead"
        register_private_key(self.web3, self.private_key)

        self.weth_address = Contract._deploy(self.web3, self.weth_abi,
                                             self.weth_bin, [])
        self.factory_address = Contract._deploy(self.web3, self.factory_abi,
                                                self.factory_bin,
                                                [self.our_address.address])
        self.router_address = Contract._deploy(
            self.web3, self.router_abi, self.router_bin,
            [self.factory_address.address, self.weth_address.address])
        self._weth_contract = Contract._get_contract(self.web3, self.weth_abi,
                                                     self.weth_address)

        self.deploy_tokens()

        token_config = {
            "tokens": {
                "DAI": {
                    "tokenAddress": self.ds_dai.address.address
                },
                "KEEP": {
                    "tokenAddress": self.ds_keep.address.address
                },
                "LEV": {
                    "tokenAddress": self.ds_lev.address.address,
                    "tokenDecimals": 9
                },
                "USDC": {
                    "tokenAddress": self.ds_usdc.address.address,
                    "tokenDecimals": 6
                },
                "WBTC": {
                    "tokenAddress": self.ds_wbtc.address.address,
                    "tokenDecimals": 8
                },
                "WETH": {
                    "tokenAddress": self.weth_address.address
                }
            }
        }
        # write token config with locally deployed addresses to file
        with open("test-token-config.json", "w+") as outfile:
            outfile.write(json.dumps(token_config))

    def deploy_tokens(self):
        self.ds_dai = DSToken.deploy(self.web3, 'DAI')
        self.ds_keep = DSToken.deploy(self.web3, 'KEEP')
        self.ds_lev = DSToken.deploy(self.web3, 'LEV')
        self.ds_usdc = DSToken.deploy(self.web3, 'USDC')
        self.ds_wbtc = DSToken.deploy(self.web3, 'WBTC')

        self.token_dai = Token("DAI", self.ds_dai.address, 18)
        self.token_keep = Token("KEEP", self.ds_keep.address, 18)
        self.token_lev = Token("LEV", self.ds_lev.address, 9)
        self.token_usdc = Token("USDC", self.ds_usdc.address, 6)
        self.token_wbtc = Token("WBTC", self.ds_wbtc.address, 8)
        self.token_weth = Token("WETH", self.weth_address, 18)

    def mint_tokens(self):
        self.ds_dai.mint(
            Wad.from_number(500)).transact(from_address=self.our_address)
        self.ds_keep.mint(
            Wad.from_number(5000)).transact(from_address=self.our_address)
        self.ds_usdc.mint(
            self.token_usdc.unnormalize_amount(
                Wad.from_number(505))).transact(from_address=self.our_address)
        self.ds_wbtc.mint(
            self.token_wbtc.unnormalize_amount(
                Wad.from_number(15))).transact(from_address=self.our_address)

    def get_target_balances(self, pair: str) -> dict:
        assert (isinstance(pair, str))

        formatted_pair = "_".join(pair.split("-")).upper()
        token_a = formatted_pair.split("_")[0]
        token_b = formatted_pair.split("_")[1]

        return {
            "min_a": TARGET_AMOUNTS[f"{formatted_pair}_MIN_{token_a}"],
            "max_a": TARGET_AMOUNTS[f"{formatted_pair}_MAX_{token_a}"],
            "min_b": TARGET_AMOUNTS[f"{formatted_pair}_MIN_{token_b}"],
            "max_b": TARGET_AMOUNTS[f"{formatted_pair}_MAX_{token_b}"]
        }

    def instantiate_keeper(self, pair: str) -> UniswapV2MarketMakerKeeper:
        if pair == "DAI-USDC":
            feed_price = "fixed:1.01"
        elif pair == "ETH-DAI":
            feed_price = "fixed:420"
        elif pair == "WBTC-USDC":
            feed_price = "fixed:12000"
        elif pair == "KEEP-ETH":
            feed_price = "fixed:0.00291025"
        elif pair == "LEV-ETH":
            feed_price = "fixed:0.00024496"

        target_balances = self.get_target_balances(pair)

        return UniswapV2MarketMakerKeeper(args=args(
            f"--eth-from {self.our_address} --rpc-host http://localhost"
            f" --rpc-port 8545"
            f" --eth-key {self.private_key}"
            f" --pair {pair}"
            f" --accepted-price-slippage-up 50"
            f" --accepted-price-slippage-down 30"
            f" --target-a-min-balance {target_balances['min_a']}"
            f" --target-a-max-balance {target_balances['max_a']}"
            f" --target-b-min-balance {target_balances['min_b']}"
            f" --target-b-max-balance {target_balances['max_b']}"
            f" --token-config ./test-token-config.json"
            f" --router-address {self.router_address.address}"
            f" --factory-address {self.factory_address.address}"
            f" --initial-delay 3"
            f" --price-feed {feed_price}"),
                                          web3=self.web3)

    def test_calculate_token_liquidity_to_add(self):
        # given
        self.mint_tokens()
        keeper = self.instantiate_keeper("DAI-USDC")
        keeper.uniswap_current_exchange_price = Wad.from_number(
            PRICES.DAI_USDC_ADD_LIQUIDITY.value)

        # when
        dai_balance = keeper.uniswap.get_account_token_balance(self.token_dai)
        usdc_balance = keeper.uniswap.get_account_token_balance(
            self.token_usdc)
        liquidity_to_add = keeper.calculate_liquidity_args(
            dai_balance, usdc_balance)

        # then
        assert all(map(lambda x: x > Wad(0), liquidity_to_add.values()))
        assert liquidity_to_add['amount_a_desired'] > liquidity_to_add[
            'amount_a_min']
        assert liquidity_to_add['amount_b_desired'] > liquidity_to_add[
            'amount_b_min']

    def test_calculate_eth_liquidity_to_add(self):
        # given
        self.mint_tokens()
        keeper = self.instantiate_keeper("ETH-DAI")
        keeper.uniswap_current_exchange_price = Wad.from_number(
            PRICES.ETH_DAI_ADD_LIQUIDITY.value)

        # when
        dai_balance = keeper.uniswap.get_account_token_balance(self.token_dai)
        eth_balance = keeper.uniswap.get_account_eth_balance()

        liquidity_to_add = keeper.calculate_liquidity_args(
            eth_balance, dai_balance)

        # then
        assert all(map(lambda x: x > Wad(0), liquidity_to_add.values()))

        assert liquidity_to_add['amount_b_desired'] > liquidity_to_add[
            'amount_b_min']
        assert liquidity_to_add['amount_a_desired'] > liquidity_to_add[
            'amount_a_min']

    def test_should_ensure_adequate_eth_for_gas(self):
        # given
        self.mint_tokens()
        keeper = self.instantiate_keeper("ETH-DAI")

        # when
        dai_balance = keeper.uniswap.get_account_token_balance(self.token_dai)
        liquidity_to_add = keeper.calculate_liquidity_args(
            Wad.from_number(0.5), dai_balance)

        # then
        assert liquidity_to_add is None

    def test_should_determine_add_liquidity(self):
        # given
        self.mint_tokens()
        keeper = self.instantiate_keeper("DAI-USDC")

        # when
        add_liquidity, remove_liquidity = keeper.determine_liquidity_action()

        # then
        assert add_liquidity == True
        assert remove_liquidity == False

    def test_should_add_dai_usdc_liquidity(self):
        # given
        self.mint_tokens()
        keeper = self.instantiate_keeper("DAI-USDC")

        initial_dai_balance = keeper.uniswap.get_account_token_balance(
            self.token_dai)
        initial_usdc_balance = keeper.uniswap.get_account_token_balance(
            self.token_usdc)

        # when
        keeper_thread = threading.Thread(target=keeper.main,
                                         daemon=True).start()

        time.sleep(10)

        added_liquidity = keeper.calculate_liquidity_args(
            initial_dai_balance, initial_usdc_balance)

        # then
        exchange_dai_balance = keeper.uniswap.get_exchange_balance(
            self.token_dai, keeper.uniswap.pair_address)
        exchange_usdc_balance = keeper.uniswap.get_exchange_balance(
            self.token_usdc, keeper.uniswap.pair_address)
        final_dai_balance = keeper.uniswap.get_account_token_balance(
            self.token_dai)
        final_usdc_balance = keeper.uniswap.get_account_token_balance(
            self.token_usdc)

        assert keeper.uniswap.get_our_exchange_balance(
            self.token_usdc, keeper.uniswap.pair_address) > Wad.from_number(0)
        assert keeper.uniswap.get_our_exchange_balance(
            self.token_dai, keeper.uniswap.pair_address) > Wad.from_number(0)
        assert initial_dai_balance > final_dai_balance
        assert initial_usdc_balance > final_usdc_balance
        assert added_liquidity['amount_a_desired'] == exchange_dai_balance
        assert self.token_usdc.normalize_amount(
            added_liquidity['amount_b_desired']) == exchange_usdc_balance

    def test_should_add_wbtc_usdc_liquidity(self):
        # given
        self.mint_tokens()
        keeper = self.instantiate_keeper("WBTC-USDC")
        initial_wbtc_balance = keeper.uniswap.get_account_token_balance(
            self.token_wbtc)
        initial_usdc_balance = keeper.uniswap.get_account_token_balance(
            self.token_usdc)

        # when
        keeper_thread = threading.Thread(target=keeper.main,
                                         daemon=True).start()

        time.sleep(10)

        added_liquidity = keeper.calculate_liquidity_args(
            initial_wbtc_balance, initial_usdc_balance)

        # then
        exchange_wbtc_balance = keeper.uniswap.get_exchange_balance(
            self.token_wbtc, keeper.uniswap.pair_address)
        exchange_usdc_balance = keeper.uniswap.get_exchange_balance(
            self.token_usdc, keeper.uniswap.pair_address)
        final_wbtc_balance = keeper.uniswap.get_account_token_balance(
            self.token_wbtc)
        final_usdc_balance = keeper.uniswap.get_account_token_balance(
            self.token_usdc)

        assert initial_wbtc_balance > final_wbtc_balance
        assert initial_usdc_balance > final_usdc_balance
        assert self.token_wbtc.normalize_amount(
            added_liquidity['amount_a_desired']) == exchange_wbtc_balance
        assert self.token_usdc.normalize_amount(
            added_liquidity['amount_b_desired']) == exchange_usdc_balance

    def test_should_add_dai_eth_liquidity(self):
        # given
        self.mint_tokens()
        keeper = self.instantiate_keeper("ETH-DAI")
        dai_balance = keeper.uniswap.get_account_token_balance(self.token_dai)
        eth_balance = keeper.uniswap.get_account_eth_balance()

        # when
        keeper_thread = threading.Thread(target=keeper.main,
                                         daemon=True).start()
        time.sleep(10)

        # then
        final_dai_balance = keeper.uniswap.get_account_token_balance(
            self.token_dai)
        final_eth_balance = keeper.uniswap.get_account_eth_balance()

        assert dai_balance > final_dai_balance
        assert eth_balance > final_eth_balance
        assert keeper.uniswap.get_our_exchange_balance(
            self.token_dai, keeper.uniswap.pair_address) > Wad.from_number(0)
        assert keeper.uniswap.get_our_exchange_balance(
            self.token_weth, keeper.uniswap.pair_address) > Wad.from_number(0)

    def test_should_remove_dai_usdc_liquidity(self):
        # given
        self.mint_tokens()
        keeper = self.instantiate_keeper("DAI-USDC")
        initial_dai_balance = keeper.uniswap.get_account_token_balance(
            self.token_dai)
        initial_usdc_balance = keeper.uniswap.get_account_token_balance(
            self.token_usdc)

        # when
        keeper_thread = threading.Thread(target=keeper.main,
                                         daemon=True).start()

        time.sleep(10)

        added_liquidity = keeper.calculate_liquidity_args(
            initial_dai_balance, initial_usdc_balance)

        post_add_exchange_dai_balance = keeper.uniswap.get_exchange_balance(
            self.token_dai, keeper.uniswap.pair_address)
        post_add_exchange_usdc_balance = keeper.uniswap.get_exchange_balance(
            self.token_usdc, keeper.uniswap.pair_address)
        post_add_dai_balance = keeper.uniswap.get_account_token_balance(
            self.token_dai)
        post_add_usdc_balance = keeper.uniswap.get_account_token_balance(
            self.token_usdc)

        assert initial_dai_balance > post_add_dai_balance
        assert initial_usdc_balance > post_add_usdc_balance
        assert added_liquidity[
            'amount_a_desired'] == post_add_exchange_dai_balance
        assert self.token_usdc.normalize_amount(
            added_liquidity['amount_b_desired']
        ) == post_add_exchange_usdc_balance

        keeper.testing_feed_price = True
        keeper.test_price = Wad.from_number(
            PRICES.DAI_USDC_REMOVE_LIQUIDITY.value)

        time.sleep(10)

        post_remove_exchange_dai_balance = keeper.uniswap.get_exchange_balance(
            self.token_dai, keeper.uniswap.pair_address)
        post_remove_exchange_usdc_balance = keeper.uniswap.get_exchange_balance(
            self.token_usdc, keeper.uniswap.pair_address)
        post_remove_dai_balance = keeper.uniswap.get_account_token_balance(
            self.token_dai)
        post_remove_usdc_balance = keeper.uniswap.get_account_token_balance(
            self.token_usdc)

        assert post_add_exchange_dai_balance > post_remove_exchange_dai_balance
        assert post_add_exchange_usdc_balance > post_remove_exchange_usdc_balance
        assert post_remove_dai_balance > post_add_dai_balance
        assert post_remove_usdc_balance > post_add_usdc_balance

    def test_should_remove_dai_eth_liquidity(self):
        # given
        self.mint_tokens()
        keeper = self.instantiate_keeper("ETH-DAI")
        initial_dai_balance = keeper.uniswap.get_account_token_balance(
            self.token_dai)
        initial_eth_balance = keeper.uniswap.get_account_eth_balance()

        # when
        keeper_thread = threading.Thread(target=keeper.main,
                                         daemon=True).start()

        time.sleep(10)

        # then
        post_add_dai_balance = keeper.uniswap.get_account_token_balance(
            self.token_dai)
        post_add_eth_balance = keeper.uniswap.get_account_eth_balance()
        post_add_exchange_dai_balance = keeper.uniswap.get_our_exchange_balance(
            self.token_dai, keeper.uniswap.pair_address)
        post_add_exchange_weth_balance = keeper.uniswap.get_our_exchange_balance(
            self.token_weth, keeper.uniswap.pair_address)

        assert initial_dai_balance > post_add_dai_balance
        assert initial_eth_balance > post_add_eth_balance

        keeper.testing_feed_price = True
        keeper.test_price = Wad.from_number(
            PRICES.ETH_DAI_REMOVE_LIQUIDITY.value)

        time.sleep(10)

        post_remove_dai_balance = keeper.uniswap.get_account_token_balance(
            self.token_dai)
        post_remove_eth_balance = keeper.uniswap.get_account_eth_balance()
        post_remove_exchange_dai_balance = keeper.uniswap.get_our_exchange_balance(
            self.token_dai, keeper.uniswap.pair_address)
        post_remove_exchange_weth_balance = keeper.uniswap.get_exchange_balance(
            self.token_weth, keeper.uniswap.pair_address)

        assert post_remove_exchange_dai_balance < post_add_exchange_dai_balance
        assert post_remove_exchange_weth_balance < post_add_exchange_weth_balance
        assert post_remove_dai_balance > post_add_dai_balance
        assert post_remove_eth_balance > post_add_eth_balance

    def test_should_remove_liquidity_if_price_feed_is_null(self):
        # given
        self.mint_tokens()
        keeper = self.instantiate_keeper("ETH-DAI")
        initial_dai_balance = keeper.uniswap.get_account_token_balance(
            self.token_dai)
        initial_eth_balance = keeper.uniswap.get_account_eth_balance()

        # when
        keeper_thread = threading.Thread(target=keeper.main,
                                         daemon=True).start()

        time.sleep(10)

        # then
        post_add_dai_balance = keeper.uniswap.get_account_token_balance(
            self.token_dai)
        post_add_eth_balance = keeper.uniswap.get_account_eth_balance()
        post_add_exchange_dai_balance = keeper.uniswap.get_our_exchange_balance(
            self.token_dai, keeper.uniswap.pair_address)
        post_add_exchange_weth_balance = keeper.uniswap.get_our_exchange_balance(
            self.token_weth, keeper.uniswap.pair_address)

        assert post_add_exchange_dai_balance > Wad.from_number(0)
        assert post_add_exchange_weth_balance > Wad.from_number(0)
        assert initial_dai_balance > post_add_dai_balance
        assert initial_eth_balance > post_add_eth_balance

        # when
        keeper.testing_feed_price = True
        keeper.test_price = None
        keeper.price_feed_accepted_delay = 2

        time.sleep(25)

        # then
        post_remove_dai_balance = keeper.uniswap.get_account_token_balance(
            self.token_dai)
        post_remove_eth_balance = keeper.uniswap.get_account_eth_balance()
        post_remove_exchange_dai_balance = keeper.uniswap.get_our_exchange_balance(
            self.token_dai, keeper.uniswap.pair_address)
        post_remove_exchange_weth_balance = keeper.uniswap.get_exchange_balance(
            self.token_weth, keeper.uniswap.pair_address)

        assert post_remove_exchange_dai_balance < post_add_exchange_dai_balance
        assert post_remove_exchange_weth_balance < post_add_exchange_weth_balance
        assert post_remove_dai_balance > post_add_dai_balance
        assert post_remove_eth_balance > post_add_eth_balance

    @unittest.skip
    def test_should_remove_liquidity_if_shutdown_signal_received(self):
        # given
        self.mint_tokens()
        keeper = self.instantiate_keeper("ETH-DAI")
        initial_dai_balance = keeper.uniswap.get_account_token_balance(
            self.token_dai)
        initial_eth_balance = keeper.uniswap.get_account_eth_balance()

        # when
        # keeper_thread = threading.Thread(target=keeper.main, daemon=True).start()
        keeper_process = Process(target=keeper.main, daemon=True).start()
        time.sleep(10)

        # then
        post_add_dai_balance = keeper.uniswap.get_account_token_balance(
            self.token_dai)
        post_add_eth_balance = keeper.uniswap.get_account_eth_balance()
        post_add_exchange_dai_balance = keeper.uniswap.get_our_exchange_balance(
            self.token_dai, keeper.uniswap.pair_address)
        post_add_exchange_weth_balance = keeper.uniswap.get_our_exchange_balance(
            self.token_weth, keeper.uniswap.pair_address)

        assert post_add_exchange_dai_balance > Wad.from_number(0)
        assert post_add_exchange_weth_balance > Wad.from_number(0)
        assert initial_dai_balance > post_add_dai_balance
        assert initial_eth_balance > post_add_eth_balance

        # when
        # send system interrupt signal to the process and wait for shutdown
        # pid = os.getpid()
        pid = keeper_process.current_process().pid
        os.kill(pid, signal.SIGINT)
        time.sleep(10)

        # then
        post_remove_dai_balance = keeper.uniswap.get_account_token_balance(
            self.token_dai)
        post_remove_eth_balance = keeper.uniswap.get_account_eth_balance()
        post_remove_exchange_dai_balance = keeper.uniswap.get_our_exchange_balance(
            self.token_dai, keeper.uniswap.pair_address)
        post_remove_exchange_weth_balance = keeper.uniswap.get_our_exchange_balance(
            self.token_weth, keeper.uniswap.pair_address)

        assert post_add_exchange_weth_balance > post_remove_exchange_dai_balance
        assert post_add_exchange_weth_balance > post_remove_exchange_weth_balance

    def test_should_remove_liquidity_if_target_amounts_are_breached(self):
        # given
        self.mint_tokens()
        keeper = self.instantiate_keeper("KEEP-ETH")
        initial_keep_balance = keeper.uniswap.get_account_token_balance(
            self.token_keep)
        initial_eth_balance = keeper.uniswap.get_account_eth_balance()

        # when
        keeper_thread = threading.Thread(target=keeper.main,
                                         daemon=True).start()
        time.sleep(10)

        # then
        post_add_keep_balance = keeper.uniswap.get_account_token_balance(
            self.token_keep)
        post_add_eth_balance = keeper.uniswap.get_account_eth_balance()
        post_add_exchange_keep_balance = keeper.uniswap.get_our_exchange_balance(
            self.token_keep, keeper.uniswap.pair_address)
        post_add_exchange_weth_balance = keeper.uniswap.get_our_exchange_balance(
            self.token_weth, keeper.uniswap.pair_address)

        assert initial_keep_balance > post_add_keep_balance
        assert initial_eth_balance > post_add_eth_balance

        # when
        # execute a swap that will break the balances target amount and wait for removal
        eth_to_swap = Wad.from_number(15)
        min_amount_out = keeper.uniswap.get_amounts_out(
            eth_to_swap, [self.token_weth, self.token_keep])

        keeper.uniswap.swap_exact_eth_for_tokens(
            eth_to_swap, min_amount_out[1],
            [self.token_weth.address.address, self.token_keep.address.address
             ]).transact()
        time.sleep(25)

        # then
        post_remove_keep_balance = keeper.uniswap.get_account_token_balance(
            self.token_keep)
        post_remove_eth_balance = keeper.uniswap.get_account_eth_balance()

        assert post_remove_keep_balance > post_add_keep_balance
        assert post_remove_eth_balance > post_add_eth_balance
        assert initial_keep_balance > post_remove_keep_balance
        assert initial_eth_balance > post_remove_eth_balance