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: # handle dusty balances with non-18-decimal tokens vat_gap = dink - vat_balance + token.min_amount if balance < vat_gap: if c.ilk.name.startswith("ETH"): wrap_eth(mcd, gal_address, vat_gap) else: raise RuntimeError("Insufficient collateral balance") assert c.adapter.join(gal_address, token.unnormalize_amount(vat_gap)).transact( from_address=gal_address) vat_balance = mcd.vat.gem(c.ilk, gal_address) balance = token.normalize_amount(c.gem.balance_of(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) urn = mcd.vat.urn(c.ilk, gal_address) # Put gal CDP at max possible debt dart = max_dart(mcd, c, gal_address) - Wad(1) if dart > Wad(0): print(f"Frobbing {c.ilk.name} with ink={urn.ink} and 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") return urn
def get_exchange_balance(self, token: Token, pair_address: Address) -> Wad: assert (isinstance(token, Token)) assert (isinstance(pair_address, Address)) return token.normalize_amount( ERC20Token(web3=self.web3, address=token.address).balance_of(pair_address))
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)
def get_exchange_balance_at_block(self, token: Token, pair_address: Address, block_number: int) -> Wad: assert (isinstance(token, Token)) assert (isinstance(pair_address, Address)) assert (isinstance(block_number, int)) return token.normalize_amount( ERC20Token(web3=self.web3, address=token.address).balance_at_block( pair_address, block_number))
def position(self, p_token: Token, pay_amount: Wad, b_token: Token, buy_amount: Wad) -> int: """Calculate the position (`pos`) new order should be inserted at to minimize gas costs. The `MatchingMarket` contract maintains an internal ordered linked list of orders, which allows the contract to do automated matching. Client placing a new order can either let the contract find the correct position in the linked list (by passing `0` as the `pos` argument of `make`) or calculate the position itself and just pass the right value to the contract (this will happen if you omit the `pos` argument of `make`). The latter should always use less gas. If the client decides not to calculate the position or it does get it wrong and the number of open orders is high at the same time, the new order may not even be placed at all as the attempt to calculate the position by the contract will likely fail due to high gas usage. This method is responsible for calculating the correct insertion position. It is used internally by `make` when `pos` argument is omitted (or is `None`). Args: p_token: Token object (see `model.py`) of the token you want to put on sale. pay_amount: Amount of the `pay_token` token you want to put on sale. b_token: Token object (see `model.py`) of the token you want to be paid with. buy_amount: Amount of the `buy_token` you want to receive. Returns: The position (`pos`) new order should be inserted at. """ assert (isinstance(p_token, Token)) assert (isinstance(pay_amount, Wad)) assert (isinstance(b_token, Token)) assert (isinstance(buy_amount, Wad)) pay_token = p_token.address buy_token = b_token.address self.logger.debug("Enumerating orders for position calculation...") orders = filter( lambda order: order.pay_amount / order.buy_amount >= p_token. normalize_amount(pay_amount) / b_token.normalize_amount(buy_amount ), self.get_orders(p_token, b_token)) self.logger.debug( "Enumerating orders for position calculation finished") sorted_orders = sorted(orders, key=lambda o: o.pay_amount / o.buy_amount) return sorted_orders[0].order_id if len(sorted_orders) > 0 else 0
class UniswapV2MarketMakerKeeper: """Keeper acting as a market maker on Uniswap v2.""" logger = logging.getLogger() send_transaction: bool = False def add_arguments(self, parser): """Provider info""" parser.add_argument( "--rpc-host", type=str, default="http://localhost:8545", help="JSON-RPC host (default: `http://localhost: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')" ) """Exchange info""" parser.add_argument( "--uniswap-router-address", type=str, required=True, help="Ethereum address of the Uniswap Router v2 contract") """Tokens info""" parser.add_argument("--first-token-address", type=str, required=True, help="Ethereum address of the first token") parser.add_argument("--first-token-name", type=str, required=True, help="name of the first token") parser.add_argument("--first-token-decimals", type=int, required=True, help="decimal of the first token") parser.add_argument("--second-token-address", type=str, required=True, help="Ethereum address of the second token") parser.add_argument("--second-token-name", type=str, required=True, help="name of the second token") parser.add_argument("--second-token-decimals", type=int, required=True, help="decimal of the second token") """settings""" 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("--max-delta-on-percent", type=float, default=3, help="Delta permissible margin") parser.add_argument( "--max-first-token-amount-input", type=float, default=10000, help= "The maximum allowed number of first tokens that can be exchanged for installation." ) parser.add_argument( "--max-second-token-amount-input", type=float, default=10000, help= "The maximum allowed number of second tokens that can be exchanged for installation." ) parser.add_argument( "--min-eth-balance", type=float, default=0, help="Minimum ETH balance below which keeper will cease operation") parser.add_argument("--min-first-token-balance", type=float, default=0, help="Minimum first token balance") parser.add_argument("--min-second-token-balance", type=float, default=0, help="Minimum second token balance") parser.add_argument("--gas-price", type=int, default=50000000000, 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") parser.add_argument( "--telegram-log-config-file", type=str, required=False, help= "config file for send logs to telegram chat (e.g. 'telegram_conf.json')", default=None) parser.add_argument( "--keeper-name", type=str, required=False, help="market maker keeper name (e.g. 'Uniswap_V2_MDTETH')", default="Uniswap_V2") def __init__(self, args: list, **kwargs): parser = argparse.ArgumentParser(prog='uniswap-market-maker-keeper') self.add_arguments(parser=parser) self.arguments = parser.parse_args(args) setup_logging(self.arguments) provider = HTTPProvider( endpoint_uri=self.arguments.rpc_host, request_kwargs={'timeout': self.arguments.rpc_timeout}) self.web3: Web3 = kwargs['web3'] if 'web3' in kwargs else Web3( provider) 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.uniswap_router = UniswapRouter( web3=self.web3, router=Address(self.arguments.uniswap_router_address)) self.first_token = ERC20Token(web3=self.web3, address=Address( self.arguments.first_token_address)) self.second_token = ERC20Token( web3=self.web3, address=Address(self.arguments.second_token_address)) self.token_first = Token(name=self.arguments.first_token_name, address=Address( self.arguments.first_token_address), decimals=self.arguments.first_token_decimals) self.token_second = Token( name=self.arguments.second_token_name, address=Address(self.arguments.second_token_address), decimals=self.arguments.second_token_decimals) self.min_eth_balance = Wad.from_number(self.arguments.min_eth_balance) self.gas_price = GasPriceFactory().create_gas_price(self.arguments) self.max_delta_on_percent = self.arguments.max_delta_on_percent self.price_feed = PriceFeedFactory().create_price_feed(self.arguments) def main(self): with Lifecycle(self.web3) as lifecycle: lifecycle.initial_delay(10) lifecycle.on_startup(self.startup) lifecycle.every(1, self.synchronize_price) def startup(self): self.approve() def approve(self): """Approve Uniswap to access our balances, so we can place orders.""" self.uniswap_router.approve([self.first_token, self.second_token], directly(gas_price=self.gas_price)) def our_available_balance(self, token: ERC20Token) -> Wad: if token.symbol() == self.token_first.name: return self.token_first.normalize_amount( token.balance_of(self.our_address)) else: return self.token_second.normalize_amount( token.balance_of(self.our_address)) @staticmethod def _get_amounts(market_price: Wad, first_token_liquidity_pool_amount: Wad, second_token_liquidity_pool_amount: Wad): liquidity_pool_constant = first_token_liquidity_pool_amount * second_token_liquidity_pool_amount new_first_token_liquidity_pool_amount = sqrt(liquidity_pool_constant * market_price) new_second_token_liquidity_pool_amount = sqrt(liquidity_pool_constant / market_price) return AttrDict({ 'exact_value': first_token_liquidity_pool_amount - Wad.from_number(new_first_token_liquidity_pool_amount), 'limit': Wad.from_number(new_second_token_liquidity_pool_amount) - second_token_liquidity_pool_amount, }) def set_price(self, market_price: Wad, first_token: Address, second_token: Address, max_delta_on_percent: int) -> Transact: pair = self.uniswap_router.get_pair(first_token=first_token, second_token=second_token) reserves = pair.reserves.map() uniswap_price = reserves[first_token] / reserves[second_token] delta = (market_price.value * 100 / uniswap_price.value) - 100 self.logger.debug(f"market price = {market_price}") self.logger.debug(f"uniswap price = {uniswap_price}") self.logger.debug( f"the percentage difference between the market price and the uniswap price = {delta}" ) if delta > max_delta_on_percent: self.logger.debug( "the price for uniswap is higher than the market price") input_data = self._get_amounts( market_price=market_price, first_token_liquidity_pool_amount=reserves[first_token], second_token_liquidity_pool_amount=reserves[second_token]) calculate_amount = self.uniswap_router.get_amounts_out( amount_in=abs(input_data.exact_value), path=[first_token, second_token]) calulate_price = abs(input_data.exact_value) / calculate_amount[-1] if abs(input_data.exact_value) > Wad.from_number( self.arguments.max_first_token_amount_input): self.logger.info( f"Amount to send first_token > maximum allowed ({abs(input_data.exact_value)} > {self.arguments.max_first_token_amount_input})" ) elif abs(input_data.exact_value) > self.first_token.balance_of( self.our_address): self.logger.warning( f"There is not enough balance to change the price " f"(required: {abs(input_data.exact_value)}), " f"balance: {self.first_token.balance_of(self.our_address)}, " f"token={self.first_token.address.address}") elif calulate_price > market_price: self.logger.info( f"new calulate price > market price ({calulate_price} > {market_price}). The price will not be changed" ) else: self.logger.info( f"To change the price, you must perform an exchange ({abs(input_data.exact_value)} {first_token.address} -> {calculate_amount[-1]} {second_token.address})" ) return self.uniswap_router.swap_from_exact_amount( amount_in=abs(input_data.exact_value), min_amount_out=calculate_amount[-1], path=[first_token, second_token]) elif delta < 0 and abs(delta) > max_delta_on_percent: self.logger.debug( "the market price is higher than the uniswap price") input_data = self._get_amounts( market_price=market_price, first_token_liquidity_pool_amount=reserves[first_token], second_token_liquidity_pool_amount=reserves[second_token]) calculate_amount = self.uniswap_router.get_amounts_in( amount_out=abs(input_data.exact_value), path=[second_token, first_token]) calulate_price = abs(input_data.exact_value) / calculate_amount[0] if calculate_amount[0] > Wad.from_number( self.arguments.max_second_token_amount_input): self.logger.info( f"Amount to send second_token > maximum allowed ({calculate_amount[0]} > {self.arguments.max_second_token_amount_input})" ) elif calculate_amount[0] > self.second_token.balance_of( self.our_address): self.logger.warning( f"There is not enough balance to change the price " f"(required: {calculate_amount[0]}), " f"balance: {self.second_token.balance_of(self.our_address)}, " f"token={self.second_token.address.address}") elif calulate_price < market_price: self.logger.info( f"new calulate price < market price ({calulate_price} < {market_price}). The price will not be changed" ) else: self.logger.info( f"To change the price, you must perform an exchange ({calculate_amount[0]} {second_token.address} -> {abs(input_data.exact_value)} {first_token.address})" ) return self.uniswap_router.swap_to_exact_amount( amount_out=abs(input_data.exact_value), max_amount_in=calculate_amount[0], path=[second_token, first_token]) else: self.logger.debug( "the price for uniswap satisfies the input accuracy. The price will not be changed" ) def synchronize_price(self): # market_maker = MarketMaker(self.uniswap_router) # 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.") return if self.first_token.balance_of(self.our_address) < Wad.from_number( self.arguments.min_first_token_balance): self.logger.warning( f"Keeper {self.token_first.name} balance below minimum.") return if self.second_token.balance_of(self.our_address) < Wad.from_number( self.arguments.min_second_token_balance): self.logger.warning( f"Keeper {self.token_second.name} balance below minimum.") return target_price = self.price_feed.get_price() transaction = self.set_price( market_price=target_price.buy_price, first_token=self.first_token.address, second_token=self.second_token.address, max_delta_on_percent=self.max_delta_on_percent) if transaction is not None: transact = transaction.transact() if transact is not None and transact.successful: self.logger.info("The price was set successfully")
def get_account_token_balance(self, token: Token) -> Wad: assert (isinstance(token, Token)) return token.normalize_amount(ERC20Token(web3=self.web3, address=token.address).balance_of(self.account_address))
def get_orders(self, p_token: Token = None, b_token: Token = None) -> List[Order]: """Get all active orders. If both `p_token` and `b_token` are specified, orders will be filtered by these. In case of the _MatchingMarket_ implementation, order enumeration will be much efficient if these two parameters are supplied, as then orders can be fetched using `getBestOffer` and a series of `getWorseOffer` calls. This approach will result in much lower number of calls comparing to the naive 0..get_last_order_id approach, especially if the number of inactive orders is very high. Either none or both of these parameters have to be specified. Args: `p_token`: Token object (see `model.py`) of the `pay_token` to filter the orders by. `b_token`: Token object (see `model.py`) of the `buy_token` to filter the orders by. Returns: A list of `Order` objects representing all active orders on Oasis. """ assert ((isinstance(p_token, Token) and isinstance(b_token, Token)) or ((p_token is None) and (b_token is None))) if (p_token is None) or (b_token is None): pay_token = None buy_token = None else: pay_token = p_token.address buy_token = b_token.address if pay_token is not None and buy_token is not None: orders = [] if self._support_contract: result = self._support_contract.functions.getOffers( self.address.address, pay_token.address, buy_token.address).call() while True: count = 0 for i in range(0, 100): if result[3][ i] != '0x0000000000000000000000000000000000000000': count += 1 orders.append( Order(market=self, order_id=result[0][i], maker=Address(result[3][i]), pay_token=pay_token, pay_amount=p_token.normalize_amount( Wad(result[1][i])), buy_token=buy_token, buy_amount=b_token.normalize_amount( Wad(result[2][i])), timestamp=result[4][i])) if count == 100: next_order_id = self._contract.functions.getWorseOffer( orders[-1].order_id).call() result = self._support_contract.functions.getOffers( self.address.address, next_order_id).call() else: break else: order_id = self._contract.functions.getBestOffer( pay_token.address, buy_token.address).call() while order_id != 0: order = self.get_order(order_id) if order is not None: orders.append(order) order_id = self._contract.functions.getWorseOffer( order_id).call() return sorted(orders, key=lambda order: order.order_id) else: return super(MatchingMarket, self).get_orders(pay_token, buy_token)
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
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