def get_basic_contract_info(self, address: ChecksumEthAddress) -> Dict[str, Any]: """ Query a contract address and return basic information as: - Decimals - name - symbol if it is provided in the contract. This method may raise: - BadFunctionCallOutput: If there is an error calling a bad address """ properties = ('decimals', 'symbol', 'name') info: Dict[str, Any] = {} contract = EthereumContract(address=address, abi=ERC20TOKEN_ABI, deployed_block=0) try: # Output contains call status and result output = multicall_2( ethereum=self, require_success=False, calls=[(address, contract.encode(method_name=prop)) for prop in properties], ) except RemoteError: # If something happens in the connection the output should have # the same length as the tuple of properties output = [(False, b'')] * len(properties) decoded = [ contract.decode(x[1], method_name)[0] # pylint: disable=E1136 if x[0] and len(x[1]) else None for (x, method_name) in zip(output, properties) ] for prop, value in zip(properties, decoded): info[prop] = value return info
def get_basic_contract_info(self, address: ChecksumEthAddress) -> Dict[str, Any]: """ Query a contract address and return basic information as: - Decimals - name - symbol if it is provided in the contract. This method may raise: - BadFunctionCallOutput: If there is an error calling a bad address """ cache = self.contract_info_cache.get(address) if cache is not None: return cache properties = ('decimals', 'symbol', 'name') info: Dict[str, Any] = {} contract = EthereumContract(address=address, abi=ERC20TOKEN_ABI, deployed_block=0) try: # Output contains call status and result output = multicall_2( ethereum=self, require_success=False, calls=[(address, contract.encode(method_name=prop)) for prop in properties], ) except RemoteError: # If something happens in the connection the output should have # the same length as the tuple of properties output = [(False, b'')] * len(properties) try: decoded = [ contract.decode(x[1], method_name)[0] # pylint: disable=E1136 if x[0] and len(x[1]) else None for (x, method_name) in zip(output, properties) ] except OverflowError as e: # This can happen when contract follows the ERC20 standard methods # but name and symbol return bytes instead of string. UNIV1 LP is in this case log.error( f'{address} failed to decode as ERC20 token. Trying UNIV1 LP token. {str(e)}', ) contract = EthereumContract(address=address, abi=UNIV1_LP_ABI, deployed_block=0) decoded = [ contract.decode(x[1], method_name)[0] # pylint: disable=E1136 if x[0] and len(x[1]) else None for (x, method_name) in zip(output, properties) ] log.debug(f'{address} was succesfuly decoded as ERC20 token') for prop, value in zip(properties, decoded): if isinstance(value, bytes): value = value.rstrip(b'\x00').decode() info[prop] = value self.contract_info_cache[address] = info return info
def find_curve_pool_price( self, lp_token: EthereumToken, ) -> Optional[Price]: """ 1. Obtain the pool for this token 2. Obtain prices for assets in pool 3. Obtain the virtual price for share and the balances of each token in the pool 4. Calc the price for a share Returns the price of 1 LP token from the pool """ assert self._ethereum is not None, 'Inquirer ethereum manager should have been initialized' # noqa: E501 pools = get_curve_pools() if lp_token.ethereum_address not in pools: return None pool = pools[lp_token.ethereum_address] tokens = [] # Translate addresses to tokens try: for asset in pool.assets: if asset == '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE': tokens.append(A_WETH) else: tokens.append(EthereumToken(asset)) except UnknownAsset: return None # Get price for each token in the pool prices = [] for token in tokens: price = self.find_usd_price(token) if price == Price(ZERO): log.error( f'Could not calculate price for {lp_token} due to inability to ' f'fetch price for {token}.', ) return None prices.append(price) # Query virtual price of LP share and balances in the pool for each token contract = EthereumContract( address=pool.pool_address, abi=CURVE_POOL_ABI, deployed_block=0, ) calls = [(pool.pool_address, contract.encode(method_name='get_virtual_price'))] calls += [(pool.pool_address, contract.encode(method_name='balances', arguments=[i])) for i in range(len(pool.assets))] output = multicall_2( ethereum=self._ethereum, require_success=False, calls=calls, ) # Check that the output has the correct structure if not all([len(call_result) == 2 for call_result in output]): log.debug( f'Failed to query contract methods while finding curve pool price. ' f'Not every outcome has length 2. {output}', ) return None # Check that all the requests were successful if not all([contract_output[0] for contract_output in output]): log.debug( f'Failed to query contract methods while finding curve price. {output}' ) return None # Deserialize information obtained in the multicall execution data = [] # https://github.com/PyCQA/pylint/issues/4739 virtual_price_decoded = contract.decode(output[0][1], 'get_virtual_price') # pylint: disable=unsubscriptable-object # noqa: E501 if not _check_curve_contract_call(virtual_price_decoded): log.debug( f'Failed to decode get_virtual_price while finding curve price. {output}' ) return None data.append(FVal(virtual_price_decoded[0])) # pylint: disable=unsubscriptable-object for i in range(len(pool.assets)): amount_decoded = contract.decode(output[i + 1][1], 'balances', arguments=[i]) if not _check_curve_contract_call(amount_decoded): log.debug( f'Failed to decode balances {i} while finding curve price. {output}' ) return None # https://github.com/PyCQA/pylint/issues/4739 amount = amount_decoded[0] # pylint: disable=unsubscriptable-object normalized_amount = token_normalized_value_decimals( amount, tokens[i].decimals) data.append(normalized_amount) # Prices and data should verify this relation for the following operations if len(prices) != len(data) - 1: log.debug( f'Length of prices {len(prices)} does not match len of data {len(data)} ' f'while querying curve pool price.', ) return None # Total number of assets price in the pool total_assets_price = sum(map(operator.mul, data[1:], prices)) if total_assets_price == 0: log.error( f'Curve pool price returned unexpected data {data} that lead to a zero price.', ) return None # Calculate weight of each asset as the proportion of tokens value weights = map(lambda x: data[x + 1] * prices[x] / total_assets_price, range(len(tokens))) assets_price = FVal(sum(map(operator.mul, weights, prices))) return (assets_price * FVal(data[0])) / (10**lp_token.decimals)
def find_uniswap_v2_lp_price( self, token: EthereumToken, ) -> Optional[Price]: """ Calculate the price for a uniswap v2 LP token. That is value = (Total value of liquidity pool) / (Current suply of LP tokens) We need: - Price of token 0 - Price of token 1 - Pooled amount of token 0 - Pooled amount of token 1 - Total supply of of pool token """ assert self._ethereum is not None, 'Inquirer ethereum manager should have been initialized' # noqa: E501 address = token.ethereum_address contract = EthereumContract(address=address, abi=UNISWAP_V2_LP_ABI, deployed_block=0) methods = [ 'token0', 'token1', 'totalSupply', 'getReserves', 'decimals' ] try: output = multicall_2( ethereum=self._ethereum, require_success=True, calls=[(address, contract.encode(method_name=method)) for method in methods], ) except RemoteError as e: log.error( f'Remote error calling multicall contract for uniswap v2 lp ' f'token {token.ethereum_address} properties: {str(e)}', ) return None # decode output decoded = [] for (method_output, method_name) in zip(output, methods): if method_output[0] and len(method_output[1]) != 0: decoded_method = contract.decode(method_output[1], method_name) if len(decoded_method) == 1: # https://github.com/PyCQA/pylint/issues/4739 decoded.append(decoded_method[0]) # pylint: disable=unsubscriptable-object else: decoded.append(decoded_method) else: log.debug( f'Multicall to Uniswap V2 LP failed to fetch field {method_name} ' f'for token {token.ethereum_address}', ) return None try: token0 = EthereumToken(decoded[0]) token1 = EthereumToken(decoded[1]) except UnknownAsset: return None try: token0_supply = FVal(decoded[3][0] * 10**-token0.decimals) token1_supply = FVal(decoded[3][1] * 10**-token1.decimals) total_supply = FVal(decoded[2] * 10**-decoded[4]) except ValueError as e: log.debug( f'Failed to deserialize token amounts for token {address} ' f'with values {str(decoded)}. f{str(e)}', ) return None token0_price = self.find_usd_price(token0) token1_price = self.find_usd_price(token1) if ZERO in (token0_price, token1_price): log.debug( f'Couldnt retrieve non zero price information for tokens {token0}, {token1} ' f'with result {token0_price}, {token1_price}', ) numerator = (token0_supply * token0_price + token1_supply * token1_price) share_value = numerator / total_supply return Price(share_value)
def get_positions( self, addresses_list: List[ChecksumEthAddress], ) -> Dict[ChecksumEthAddress, Trove]: contract = EthereumContract( address=LIQUITY_TROVE_MANAGER.address, abi=LIQUITY_TROVE_MANAGER.abi, deployed_block=LIQUITY_TROVE_MANAGER.deployed_block, ) # make a copy of the list to avoid modifications in the list that is passed as argument addresses = list(addresses_list) proxied_addresses = self._get_accounts_having_proxy() proxies_to_address = {v: k for k, v in proxied_addresses.items()} addresses += proxied_addresses.values() calls = [(LIQUITY_TROVE_MANAGER.address, contract.encode(method_name='Troves', arguments=[x])) for x in addresses] outputs = multicall_2( ethereum=self.ethereum, require_success=False, calls=calls, ) data: Dict[ChecksumEthAddress, Trove] = {} eth_price = Inquirer().find_usd_price(A_ETH) lusd_price = Inquirer().find_usd_price(A_LUSD) for idx, output in enumerate(outputs): status, result = output if status is True: try: trove_info = contract.decode(result, 'Troves', arguments=[addresses[idx]]) trove_is_active = bool(trove_info[3]) # pylint: disable=unsubscriptable-object if not trove_is_active: continue collateral = deserialize_asset_amount( token_normalized_value_decimals(trove_info[1], 18), # noqa: E501 pylint: disable=unsubscriptable-object ) debt = deserialize_asset_amount( token_normalized_value_decimals(trove_info[0], 18), # noqa: E501 pylint: disable=unsubscriptable-object ) collateral_balance = AssetBalance( asset=A_ETH, balance=Balance( amount=collateral, usd_value=eth_price * collateral, ), ) debt_balance = AssetBalance( asset=A_LUSD, balance=Balance( amount=debt, usd_value=lusd_price * debt, ), ) # Avoid division errors collateralization_ratio: Optional[FVal] liquidation_price: Optional[FVal] if debt > 0: collateralization_ratio = eth_price * collateral / debt * 100 else: collateralization_ratio = None if collateral > 0: liquidation_price = debt * lusd_price * FVal( MIN_COLL_RATE) / collateral else: liquidation_price = None account_address = addresses[idx] if account_address in proxies_to_address: account_address = proxies_to_address[account_address] data[account_address] = Trove( collateral=collateral_balance, debt=debt_balance, collateralization_ratio=collateralization_ratio, liquidation_price=liquidation_price, active=trove_is_active, trove_id=trove_info[4], # pylint: disable=unsubscriptable-object ) except DeserializationError as e: self.msg_aggregator.add_warning( f'Ignoring Liquity trove information. ' f'Failed to decode contract information. {str(e)}.', ) return data
def get_dill_balances( self, addresses: List[ChecksumEthAddress], ) -> Dict[ChecksumEthAddress, DillBalance]: """ Query information for amount locked, pending rewards and time until unlock for Pickle's dill. """ api_output = {} rewards_calls = [( PICKLE_DILL_REWARDS.address, self.rewards_contract.encode(method_name='claim', arguments=[x]), ) for x in addresses] balance_calls = [(PICKLE_DILL.address, self.dill_contract.encode(method_name='locked', arguments=[x])) for x in addresses] outputs = multicall_2( ethereum=self.ethereum, require_success=False, calls=rewards_calls + balance_calls, ) reward_outputs, dill_outputs = outputs[:len(addresses)], outputs[ len(addresses):] pickle_price = Inquirer().find_usd_price(A_PICKLE) for idx, output in enumerate(reward_outputs): status_rewards, result = output status_dill, result_dill = dill_outputs[idx] address = addresses[idx] if all((status_rewards, status_dill)): try: rewards = self.rewards_contract.decode(result, 'claim', arguments=[address]) dill_amounts = self.dill_contract.decode( result_dill, 'locked', arguments=[address], ) dill_rewards = token_normalized_value_decimals( token_amount=rewards[0], # pylint: disable=unsubscriptable-object token_decimals=A_PICKLE.decimals, ) dill_locked = token_normalized_value_decimals( token_amount=dill_amounts[0], # pylint: disable=unsubscriptable-object token_decimals=A_PICKLE.decimals, ) balance = DillBalance( dill_amount=AssetBalance( asset=A_PICKLE, balance=Balance( amount=dill_locked, usd_value=pickle_price * dill_locked, ), ), pending_rewards=AssetBalance( asset=A_PICKLE, balance=Balance( amount=dill_rewards, usd_value=pickle_price * dill_rewards, ), ), lock_time=deserialize_timestamp(dill_amounts[1]), # noqa: E501 pylint: disable=unsubscriptable-object ) api_output[address] = balance except (DeserializationError, IndexError) as e: self.msg_aggregator.add_error( f'Failed to query dill information for address {address}. {str(e)}', ) return api_output