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 __init__(self, args: list, **kwargs): parser = argparse.ArgumentParser(prog='Mooniswap-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.mooniswap_factory = MooniFactory( web3=self.web3, factory_address=Address(self.arguments.mooniswap_factory_address)) first_token_address = Address(self.arguments.first_token_address) second_token_address = Address(self.arguments.second_token_address) if first_token_address == Address( "0x0000000000000000000000000000000000000000"): self.first_token = EthToken(web3=self.web3, address=first_token_address) else: self.first_token = ERC20Token( web3=self.web3, address=Address(self.arguments.first_token_address)) if second_token_address == Address( "0x0000000000000000000000000000000000000000"): self.second_token = EthToken(web3=self.web3, address=second_token_address) else: self.second_token = ERC20Token( web3=self.web3, address=Address(self.arguments.second_token_address)) self.referral = Address(self.arguments.mooniswap_referral_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.mooniswap = self.mooniswap_factory.get_pair( first_token=self.token_first.address, second_token=self.token_second.address) 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 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)
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)
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 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)
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)
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 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, 18) self.token2.mint(Wad.from_number(10000)).transact() self.token3 = DSToken.deploy(self.web3, 'CCC') self.token3_tokenclass = Token('CCC', self.token3.address, 18) self.token3.mint(Wad.from_number(10000)).transact() self.otc = None
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 instantiate_tokens(self, pair: str) -> Tuple[Token, Token]: assert (isinstance(pair, str)) def get_address(value) -> Address: return Address(value['tokenAddress']) if 'tokenAddress' in value else None def get_decimals(value) -> int: return value['tokenDecimals'] if 'tokenDecimals' in value else 18 token_a_name = 'WETH' if pair.split('-')[0] == 'ETH' or pair.split('-')[0] == 'WETH' else pair.split('-')[0] token_b_name = 'WETH' if pair.split('-')[1] == 'ETH' or pair.split('-')[1] == 'WETH' else pair.split('-')[1] token_a = Token(token_a_name, get_address(self.token_config[token_a_name]), get_decimals(self.token_config[token_a_name])) token_b = Token(token_b_name, get_address(self.token_config[token_b_name]), get_decimals(self.token_config[token_b_name])) return token_a, token_b
def set_pair_token(self, pair_address: Address): self.pair_address = pair_address self._pair_contract = self._get_contract(self.web3, self.pair_abi['abi'], self.pair_address) self.pair_token = Token('Liquidity', self.pair_address, 18) self.is_new_pool = False
def instantiate_tokens(self, pair: str) -> Tuple[Token, Token]: assert (isinstance(pair, str)) def get_address(value) -> Address: return Address(value['tokenAddress']) if 'tokenAddress' in value else None def get_decimals(value) -> int: return value['tokenDecimals'] if 'tokenDecimals' in value else 18 token_a_name = 'WETH' if self.is_eth and self.eth_position == 0 else self.pair().split('-')[0] token_b_name = 'WETH' if self.is_eth and self.eth_position == 1 else self.pair().split('-')[1] token_a = Token(token_a_name, get_address(self.token_config[token_a_name]), get_decimals(self.token_config[token_a_name])) token_b = Token(token_b_name, get_address(self.token_config[token_b_name]), get_decimals(self.token_config[token_b_name])) return token_a, token_b
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))
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.mint(Wad.from_number(10000)).transact() self.token1_tokenclass = Token('AAA', self.token1.address, 18) self.token2 = DSToken.deploy(self.web3, 'BBB') self.token2.mint(Wad.from_number(10000)).transact() self.token2_tokenclass = Token('BBB', self.token2.address, 18) price_oracle = OasisMockPriceOracle.deploy(self.web3) self.otc = MatchingMarket.deploy(self.web3, self.token1.address, Wad(0), price_oracle.address) self.otc.add_token_pair_whitelist(self.token1.address, self.token2.address).transact() self.otc.approve([self.token1, self.token2], directly()) for amount in [11,55,44,34,36,21,45,51,15]: self.otc.make(p_token=self.token1_tokenclass, pay_amount=Wad.from_number(1), b_token=self.token2_tokenclass, buy_amount=Wad.from_number(amount)).transact()
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 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 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, []) price_oracle = OasisMockPriceOracle.deploy(self.web3) self.otc = MatchingMarket.deploy(self.web3, self.token1.address, Wad(0), price_oracle.address, support_address) self.otc.add_token_pair_whitelist(self.token1.address, self.token2.address).transact() self.otc.approve([self.token1, self.token2], directly())
def __init__(self, data: dict): assert (isinstance(data, dict)) self.tokens = [ Token(name=key, address=Address(value['tokenAddress']) if 'tokenAddress' in value else None, decimals=value['tokenDecimals'] if 'tokenDecimals' in value else 18) for key, value in data['tokens'].items() ]
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 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 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.ds_dai = DSToken.deploy(self.web3, 'DAI') self.ds_dai.mint( Wad.from_number(500)).transact(from_address=self.our_address) self.token_dai = Token("DAI", self.ds_dai.address, 18) self.token_weth = Token("WETH", self.weth_address, 18) token_config = { "tokens": { "DAI": { "tokenAddress": self.ds_dai.address.address }, "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 setup_method(self): # reduce logspew logging.getLogger("web3").setLevel(logging.INFO) logging.getLogger("urllib3").setLevel(logging.INFO) logging.getLogger("asyncio").setLevel(logging.INFO) 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.price_oracle = OasisMockPriceOracle.deploy(self.web3) self.price_oracle.set_price(Wad.from_number(10)) 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, 18) self.token2.mint(Wad.from_number(10000)).transact() self.token3 = DSToken.deploy(self.web3, 'CCC') self.token3_tokenclass = Token('CCC', self.token3.address, 18) self.token3.mint(Wad.from_number(10000)).transact() self.otc = None
def deploy_liquidity_token(self) -> Token: # deploy uniswap contracts 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) # deploy dai contract and instantiate DAI token self.ds_dai = DSToken.deploy(self.web3, 'DAI') self.token_dai = Token("DAI", self.ds_dai.address, 18) 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) # add liquidity self.add_liquidity_eth() # set liquidity token self.dai_eth_uniswap.set_pair_token(self.dai_eth_uniswap.get_pair_address(self.token_dai.address, self.token_weth.address)) liquidity_token = self.dai_eth_uniswap.pair_token return liquidity_token
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
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.ds_reward_token = DSToken.deploy(self.web3, 'REWARD') self.reward_token = Token("REWARD", self.ds_reward_token.address, 18) # Deploy UniswapV2 contracts and set liquidity token self.liquidity_token = self.deploy_liquidity_token() self.staking_rewards_address = self._deploy(self.web3, self.staking_rewards_abi, self.staking_rewards_bin, [self.our_address.address, self.reward_token.address.address, self.liquidity_token.address.address]) self.uniswap_staking_rewards = UniswapStakingRewards(self.web3, self.our_address, Address(self.staking_rewards_address), "UniswapStakingRewards") ## Useful for debugging failing transactions logger = logging.getLogger('eth') logger.setLevel(8)
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
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)
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")
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