def pairs_from_ethereum(ethereum: EthereumManager) -> Dict[str, Any]: """Detect the uniswap v2 pool tokens by using an ethereum node""" contracts_file = Path(__file__).resolve().parent / 'contracts.json' with contracts_file.open('r') as f: contracts = json.loads(f.read()) univ2factory = EthereumContract( address=contracts['UNISWAPV2FACTORY']['address'], abi=contracts['UNISWAPV2FACTORY']['abi'], deployed_block=0, # whatever ) pairs_num = univ2factory.call(ethereum, 'allPairsLength') chunks = list(get_chunks([[x] for x in range(pairs_num)], n=500)) pairs = [] for idx, chunk in enumerate(chunks): print(f'Querying univ2 pairs chunk {idx + 1} / {len(chunks)}') result = multicall_specific(ethereum, univ2factory, 'allPairs', chunk) try: pairs.extend([deserialize_ethereum_address(x[0]) for x in result]) except DeserializationError: print( 'Error deserializing address while fetching uniswap v2 pool tokens' ) sys.exit(1) return pairs
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 find_yearn_price( self, token: EthereumToken, ) -> Optional[Price]: """ Query price for a yearn vault v2 token using the pricePerShare method and the price of the underlying token. """ assert self._ethereum is not None, 'Inquirer ethereum manager should have been initialized' # noqa: E501 maybe_underlying_token = GlobalDBHandler().fetch_underlying_tokens( token.ethereum_address) if maybe_underlying_token is None or len(maybe_underlying_token) != 1: log.error(f'Yearn vault token {token} without an underlying asset') return None underlying_token = EthereumToken(maybe_underlying_token[0].address) underlying_token_price = self.find_usd_price(underlying_token) # Get the price per share from the yearn contract contract = EthereumContract( address=token.ethereum_address, abi=YEARN_VAULT_V2_ABI, deployed_block=0, ) try: price_per_share = contract.call(self._ethereum, 'pricePerShare') return Price(price_per_share * underlying_token_price / 10**token.decimals) except (RemoteError, BlockchainQueryError) as e: log.error( f'Failed to query pricePerShare method in Yearn v2 Vault. {str(e)}' ) return None
def _decode_ens_contract(params: EnsContractParams, result_encoded: Any) -> ChecksumEthAddress: contract = EthereumContract(address=params.address, abi=params.abi, deployed_block=0) result = contract.decode( # pylint: disable=E1136 result=result_encoded, method_name=params.method_name, arguments=params.arguments, )[0] return string_to_ethereum_address(result)
def __init__( self, ethereum_manager: 'EthereumManager', msg_aggregator: MessagesAggregator, ) -> None: self.ethereum = ethereum_manager self.msg_aggregator = msg_aggregator self.contract = EthereumContract( address=ZERION_ADAPTER_ADDRESS, abi=ZERION_ABI, deployed_block=1586199170, )
def multicall_specific( ethereum: 'EthereumManager', contract: EthereumContract, method_name: str, arguments: List[Any], call_order: Optional[Sequence['NodeName']] = None, ) -> Any: calls = [( contract.address, contract.encode(method_name=method_name, arguments=i), ) for i in arguments] output = multicall(ethereum, calls, call_order) return [contract.decode(x, method_name, arguments[0]) for x in output]
def uniswap_lp_token_balances( userdb: 'DBHandler', address: ChecksumEthAddress, ethereum: 'EthereumManager', lp_addresses: List[ChecksumEthAddress], known_assets: Set[EthereumToken], unknown_assets: Set[EthereumToken], ) -> List[LiquidityPool]: """Query uniswap token balances from ethereum chain The number of addresses to query in one call depends a lot on the node used. With an infura node we saw the following: 500 addresses per call took on average 43 seconds for 20450 addresses 2000 addresses per call took on average 36 seconds for 20450 addresses 4000 addresses per call took on average 32.6 seconds for 20450 addresses 5000 addresses timed out a few times """ zerion_contract = EthereumContract( address=ZERION_ADAPTER_ADDRESS, abi=ZERION_ABI, deployed_block=1586199170, ) if NodeName.OWN in ethereum.web3_mapping: chunks = list(get_chunks(lp_addresses, n=4000)) call_order = [NodeName.OWN] else: chunks = list(get_chunks(lp_addresses, n=700)) call_order = ethereum.default_call_order(skip_etherscan=True) balances = [] for chunk in chunks: result = zerion_contract.call( ethereum=ethereum, method_name='getAdapterBalance', arguments=[ address, '0x4EdBac5c8cb92878DD3fd165e43bBb8472f34c3f', chunk ], call_order=call_order, ) for entry in result[1]: balances.append( _decode_result(userdb, entry, known_assets, unknown_assets)) return balances
def __init__( # pylint: disable=super-init-not-called self, ethereum_manager: 'EthereumManager', base_tools: 'BaseDecoderTools', msg_aggregator: 'MessagesAggregator', # pylint: disable=unused-argument ) -> None: self.base = base_tools self.ethereum = ethereum_manager dir_path = os.path.dirname(os.path.realpath(__file__)) with open(os.path.join(dir_path, 'data', 'contracts.json'), 'r') as f: contracts = json.loads(f.read()) self.contract = EthereumContract( address=contracts['DXDAOMESA']['address'], abi=contracts['DXDAOMESA']['abi'], deployed_block=contracts['DXDAOMESA']['deployed_block'], )
def get_pool_price( self, pool_addr: ChecksumEthAddress, block_identifier: BlockIdentifier = 'latest', ) -> PoolPrice: """ Returns the units of token1 that one token0 can buy """ pool_contract = EthereumContract( address=pool_addr, abi=UNISWAP_V3_POOL_ABI, deployed_block=UNISWAP_FACTORY_DEPLOYED_BLOCK, ) calls = [ ( pool_contract.address, pool_contract.encode(method_name='slot0'), ), ( pool_contract.address, pool_contract.encode(method_name='token0'), ), ( pool_contract.address, pool_contract.encode(method_name='token1'), ), ] output = multicall( ethereum=self.eth_manager, calls=calls, require_success=True, block_identifier=block_identifier, ) token_0 = EthereumToken( to_checksum_address(pool_contract.decode(output[1], 'token0')[0]), # noqa: E501 pylint:disable=unsubscriptable-object ) token_1 = EthereumToken( to_checksum_address(pool_contract.decode(output[2], 'token1')[0]), # noqa: E501 pylint:disable=unsubscriptable-object ) sqrt_price_x96, _, _, _, _, _, _ = pool_contract.decode( output[0], 'slot0') decimals_constant = 10**(token_0.decimals - token_1.decimals) price = FVal( (sqrt_price_x96 * sqrt_price_x96) / 2**(192) * decimals_constant) if ZERO == price: raise DefiPoolError( f'Uniswap pool for {token_0}/{token_1} has price 0') return PoolPrice(price=price, token_0=token_0, token_1=token_1)
def __init__( self, ethereum_manager: 'EthereumManager', database: 'DBHandler', premium: Optional[Premium], msg_aggregator: MessagesAggregator, ) -> None: self.ethereum = ethereum_manager self.database = database self.premium = premium self.msg_aggregator = msg_aggregator self.rewards_contract = EthereumContract( address=PICKLE_DILL_REWARDS.address, abi=PICKLE_DILL_REWARDS.abi, deployed_block=PICKLE_DILL_REWARDS.deployed_block, ) self.dill_contract = EthereumContract( address=PICKLE_DILL.address, abi=PICKLE_DILL.abi, deployed_block=PICKLE_DILL.deployed_block, )
def get_pool_price( self, pool_addr: ChecksumEthAddress, block_identifier: BlockIdentifier = 'latest', ) -> PoolPrice: """ Returns the units of token1 that one token0 can buy """ pool_contract = EthereumContract( address=pool_addr, abi=UNISWAP_V2_LP_ABI, deployed_block=10000835, # Factory deployment block ) calls = [ ( pool_contract.address, pool_contract.encode(method_name='getReserves'), ), ( pool_contract.address, pool_contract.encode(method_name='token0'), ), ( pool_contract.address, pool_contract.encode(method_name='token1'), ), ] output = multicall( ethereum=self.eth_manager, calls=calls, require_success=True, block_identifier=block_identifier, ) token_0 = EthereumToken( to_checksum_address(pool_contract.decode(output[1], 'token0')[0]), # noqa: E501 pylint:disable=unsubscriptable-object ) token_1 = EthereumToken( to_checksum_address(pool_contract.decode(output[2], 'token1')[0]), # noqa: E501 pylint:disable=unsubscriptable-object ) reserve_0, reserve_1, _ = pool_contract.decode(output[0], 'getReserves') decimals_constant = 10**(token_0.decimals - token_1.decimals) if ZERO in (reserve_0, reserve_1): raise DefiPoolError( f'Uniswap pool for {token_0}/{token_1} has asset with no reserves' ) price = FVal((reserve_1 / reserve_0) * decimals_constant) return PoolPrice(price=price, token_0=token_0, token_1=token_1)
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 contract_or_none(name: str) -> Optional[EthereumContract]: """Gets details of an ethereum contract from the contracts json file Returns None if missing """ contract = EthereumConstants().contracts.get(name, None) if contract is None: return None return EthereumContract( address=contract['address'], abi=contract['abi'], deployed_block=contract['deployed_block'], )
def get_pool( self, token_0: EthereumToken, token_1: EthereumToken, ) -> List[str]: result = multicall_specific( ethereum=self.eth_manager, contract=UNISWAP_V3_FACTORY, method_name='getPool', arguments=[[ token_0.ethereum_address, token_1.ethereum_address, fee, ] for fee in (3000, 500, 10000)], ) # get liquidity for each pool and choose the pool with the highest liquidity best_pool, max_liquidity = to_checksum_address(result[0][0]), 0 for query in result: if query[0] == ZERO_ADDRESS: continue pool_address = to_checksum_address(query[0]) pool_contract = EthereumContract( address=pool_address, abi=UNISWAP_V3_POOL_ABI, deployed_block=UNISWAP_FACTORY_DEPLOYED_BLOCK, ) pool_liquidity = pool_contract.call( ethereum=self.eth_manager, method_name='liquidity', arguments=[], call_order=None, ) if pool_liquidity > max_liquidity: best_pool = pool_address return [best_pool]
def _encode_ens_contract(params: EnsContractParams) -> str: contract = EthereumContract(address=params.address, abi=params.abi, deployed_block=0) return contract.encode(method_name=params.method_name, arguments=params.arguments)
def find_uniswap_v2_lp_price( ethereum: 'EthereumManager', token: EthereumToken, token_price_func: Callable, token_price_func_args: List[Any], block_identifier: BlockIdentifier, ) -> 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 """ address = token.ethereum_address contract = EthereumContract(address=address, abi=UNISWAP_V2_LP_ABI, deployed_block=0) methods = ['token0', 'token1', 'totalSupply', 'getReserves', 'decimals'] multicall_method = multicall_2 # choose which multicall to use if isinstance(block_identifier, int): if block_identifier <= 7929876: log.error( f'No multicall contract at the block {block_identifier}. Uniswap v2 LP ' f'query failed. Should implement direct queries', ) return None if block_identifier <= 12336033: multicall_method = multicall try: output = multicall_method( ethereum=ethereum, require_success=True, calls=[(address, contract.encode(method_name=method)) for method in methods], block_identifier=block_identifier, ) 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): call_success = True if multicall_method == multicall_2: call_success = method_output[0] call_result = method_output[1] else: call_result = method_output # type: ignore if call_success and len(call_result) != 0: decoded_method = contract.decode(call_result, 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 = token_price_func(token0, *token_price_func_args) token1_price = token_price_func(token1, *token_price_func_args) 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)
class ZerionSDK(): """Adapter for the Zerion DeFi SDK https://github.com/zeriontech/defi-sdk""" def __init__( self, ethereum_manager: 'EthereumManager', msg_aggregator: MessagesAggregator, ) -> None: self.ethereum = ethereum_manager self.msg_aggregator = msg_aggregator self.contract = EthereumContract( address=ZERION_ADAPTER_ADDRESS, abi=ZERION_ABI, deployed_block=1586199170, ) def all_balances_for_account( self, account: ChecksumEthAddress) -> List[DefiProtocolBalances]: """Calls the contract's getBalances() to get all protocol balances for account https://docs.zerion.io/smart-contracts/adapterregistry-v3#getbalances """ result = self.contract.call( ethereum=self.ethereum, method_name='getBalances', arguments=[account], ) protocol_balances = [] for entry in result: protocol = DefiProtocol( name=entry[0][0], description=entry[0][1], url=entry[0][2], version=entry[0][4], ) for adapter_balance in entry[1]: balance_type = adapter_balance[0][ 1] # can be either 'Asset' or 'Debt' for balances in adapter_balance[1]: underlying_balances = [] base_balance = self._get_single_balance( protocol.name, balances[0]) for balance in balances[1]: defi_balance = self._get_single_balance( protocol.name, balance) underlying_balances.append(defi_balance) if base_balance.balance.usd_value == ZERO: # This can happen. We can't find a price for some assets # such as combined pool assets. But we can instead use # the sum of the usd_value of the underlying_balances usd_sum = sum(x.balance.usd_value for x in underlying_balances) base_balance.balance.usd_value = usd_sum # type: ignore protocol_balances.append( DefiProtocolBalances( protocol=protocol, balance_type=balance_type, base_balance=base_balance, underlying_balances=underlying_balances, )) return protocol_balances def _get_single_balance( self, protocol_name: str, entry: Tuple[Tuple[str, str, str, int], int], ) -> DefiBalance: metadata = entry[0] balance_value = entry[1] decimals = metadata[3] normalized_value = token_normalized_value_decimals( balance_value, decimals) token_symbol = metadata[2] token_address = to_checksum_address(metadata[0]) token_name = metadata[1] special_handling = self.handle_protocols( protocol_name=protocol_name, token_symbol=token_symbol, normalized_balance=normalized_value, token_address=token_address, token_name=token_name, ) if special_handling: return special_handling try: asset = Asset(token_symbol) usd_price = Inquirer().find_usd_price(asset) except (UnknownAsset, UnsupportedAsset): if not _is_token_non_standard(token_symbol, token_address): self.msg_aggregator.add_warning( f'Unsupported asset {token_symbol} with address ' f'{token_address} encountered during DeFi protocol queries', ) usd_price = Price(ZERO) usd_value = normalized_value * usd_price defi_balance = DefiBalance( token_address=token_address, token_name=token_name, token_symbol=token_symbol, balance=Balance(amount=normalized_value, usd_value=usd_value), ) return defi_balance def handle_protocols( self, protocol_name: str, token_symbol: str, normalized_balance: FVal, token_address: str, token_name: str, ) -> Optional[DefiBalance]: """Special handling for price for token/protocols which are easier to do onchain or need some kind of special treatment. """ if protocol_name == 'PoolTogether': result = _handle_pooltogether(normalized_balance, token_name) if result is not None: return result underlying_asset_price = get_underlying_asset_price(token_symbol) usd_price = handle_defi_price_query(self.ethereum, token_symbol, underlying_asset_price) if usd_price is None: return None return DefiBalance( token_address=to_checksum_address(token_address), token_name=token_name, token_symbol=token_symbol, balance=Balance(amount=normalized_balance, usd_value=normalized_balance * usd_price), )
class ZerionSDK(): """Adapter for the Zerion DeFi SDK https://github.com/zeriontech/defi-sdk""" def __init__( self, ethereum_manager: 'EthereumManager', msg_aggregator: MessagesAggregator, ) -> None: self.ethereum = ethereum_manager self.msg_aggregator = msg_aggregator self.contract = EthereumContract( address=ZERION_ADAPTER_ADDRESS, abi=ZERION_ABI, deployed_block=1586199170, ) self.protocol_names: Optional[List[str]] = None def _get_protocol_names(self) -> List[str]: if self.protocol_names is not None: return self.protocol_names try: protocol_names = self.contract.call( ethereum=self.ethereum, method_name='getProtocolNames', arguments=[], ) except RemoteError as e: log.warning( f'Failed to query zerion defi sdk for protocol names due to {str(e)}' f'Falling back to known list of names.', ) return list(KNOWN_ZERION_PROTOCOL_NAMES) self.protocol_names = protocol_names return protocol_names def _query_chain_for_all_balances(self, account: ChecksumEthAddress) -> List: if NodeName.OWN in self.ethereum.web3_mapping: try: # In this case we don't care about the gas limit return self.contract.call( ethereum=self.ethereum, method_name='getBalances', arguments=[account], call_order=[NodeName.OWN, NodeName.ONEINCH], ) except RemoteError: log.warning( 'Failed to query zerionsdk balances with own node. Falling ' 'back to multiple calls to getProtocolBalances', ) # but if we are not connected to our own node the zerion sdk get balances call # has unfortunately crossed the default limits of almost all open nodes apart from 1inch # https://github.com/rotki/rotki/issues/1969 # So now we get all supported protocols and query in batches protocol_names = self._get_protocol_names() result = [] protocol_chunks: List[List[str]] = list( get_chunks( list(protocol_names), n=PROTOCOLS_QUERY_NUM, )) for protocol_names in protocol_chunks: contract_result = self.contract.call( ethereum=self.ethereum, method_name='getProtocolBalances', arguments=[account, protocol_names], ) if len(contract_result) == 0: continue result.extend(contract_result) return result def all_balances_for_account( self, account: ChecksumEthAddress) -> List[DefiProtocolBalances]: """Calls the contract's getBalances() to get all protocol balances for account https://docs.zerion.io/smart-contracts/adapterregistry-v3#getbalances """ result = self._query_chain_for_all_balances(account=account) protocol_balances = [] for entry in result: protocol = DefiProtocol( name=entry[0][0], description=entry[0][1], url=entry[0][2], version=entry[0][4], ) for adapter_balance in entry[1]: balance_type = adapter_balance[0][ 1] # can be either 'Asset' or 'Debt' for balances in adapter_balance[1]: underlying_balances = [] base_balance = self._get_single_balance( protocol.name, balances[0]) for balance in balances[1]: defi_balance = self._get_single_balance( protocol.name, balance) underlying_balances.append(defi_balance) if base_balance.balance.usd_value == ZERO: # This can happen. We can't find a price for some assets # such as combined pool assets. But we can instead use # the sum of the usd_value of the underlying_balances usd_sum = sum(x.balance.usd_value for x in underlying_balances) base_balance.balance.usd_value = usd_sum # type: ignore protocol_balances.append( DefiProtocolBalances( protocol=protocol, balance_type=balance_type, base_balance=base_balance, underlying_balances=underlying_balances, )) return protocol_balances def _get_single_balance( self, protocol_name: str, entry: Tuple[Tuple[str, str, str, int], int], ) -> DefiBalance: metadata = entry[0] balance_value = entry[1] decimals = metadata[3] normalized_value = token_normalized_value_decimals( balance_value, decimals) token_symbol = metadata[2] token_address = to_checksum_address(metadata[0]) token_name = metadata[1] special_handling = self.handle_protocols( protocol_name=protocol_name, token_symbol=token_symbol, normalized_balance=normalized_value, token_address=token_address, token_name=token_name, ) if special_handling: return special_handling try: asset = Asset(token_symbol) usd_price = Inquirer().find_usd_price(asset) except (UnknownAsset, UnsupportedAsset): if not _is_token_non_standard(token_symbol, token_address): self.msg_aggregator.add_warning( f'Unsupported asset {token_symbol} with address ' f'{token_address} encountered during DeFi protocol queries', ) usd_price = Price(ZERO) usd_value = normalized_value * usd_price defi_balance = DefiBalance( token_address=token_address, token_name=token_name, token_symbol=token_symbol, balance=Balance(amount=normalized_value, usd_value=usd_value), ) return defi_balance def handle_protocols( self, protocol_name: str, token_symbol: str, normalized_balance: FVal, token_address: str, token_name: str, ) -> Optional[DefiBalance]: """Special handling for price for token/protocols which are easier to do onchain or need some kind of special treatment. """ if protocol_name == 'PoolTogether': result = _handle_pooltogether(normalized_balance, token_name) if result is not None: return result underlying_asset_price = get_underlying_asset_price(token_symbol) usd_price = handle_defi_price_query(self.ethereum, token_symbol, underlying_asset_price) if usd_price is None: return None return DefiBalance( token_address=to_checksum_address(token_address), token_name=token_name, token_symbol=token_symbol, balance=Balance(amount=normalized_balance, usd_value=normalized_balance * usd_price), )
class PickleFinance(EthereumModule): def __init__( self, ethereum_manager: 'EthereumManager', database: 'DBHandler', premium: Optional[Premium], msg_aggregator: MessagesAggregator, ) -> None: self.ethereum = ethereum_manager self.database = database self.premium = premium self.msg_aggregator = msg_aggregator self.rewards_contract = EthereumContract( address=PICKLE_DILL_REWARDS.address, abi=PICKLE_DILL_REWARDS.abi, deployed_block=PICKLE_DILL_REWARDS.deployed_block, ) self.dill_contract = EthereumContract( address=PICKLE_DILL.address, abi=PICKLE_DILL.abi, deployed_block=PICKLE_DILL.deployed_block, ) 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 def balances_in_protocol( self, addresses: List[ChecksumEthAddress], ) -> Dict[ChecksumEthAddress, List['AssetBalance']]: """Queries all the pickles deposited and available to claim in the protocol""" dill_balances = self.get_dill_balances(addresses) balances_per_address: Dict[ChecksumEthAddress, List['AssetBalance']] = defaultdict(list) for address, dill_balance in dill_balances.items(): pickles = dill_balance.dill_amount + dill_balance.pending_rewards if pickles.balance.amount != 0: balances_per_address[address] += [pickles] return balances_per_address # -- Methods following the EthereumModule interface -- # def on_startup(self) -> None: pass def on_account_addition( self, address: ChecksumEthAddress) -> Optional[List['AssetBalance']]: return self.balances_in_protocol([address]).get(address, None) def on_account_removal(self, address: ChecksumEthAddress) -> None: pass def deactivate(self) -> None: pass
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
class DxdaomesaDecoder(DecoderInterface): # lgtm[py/missing-call-to-init] def __init__( # pylint: disable=super-init-not-called self, ethereum_manager: 'EthereumManager', base_tools: 'BaseDecoderTools', msg_aggregator: 'MessagesAggregator', # pylint: disable=unused-argument ) -> None: self.base = base_tools self.ethereum = ethereum_manager dir_path = os.path.dirname(os.path.realpath(__file__)) with open(os.path.join(dir_path, 'data', 'contracts.json'), 'r') as f: contracts = json.loads(f.read()) self.contract = EthereumContract( address=contracts['DXDAOMESA']['address'], abi=contracts['DXDAOMESA']['abi'], deployed_block=contracts['DXDAOMESA']['deployed_block'], ) def _decode_events( # pylint: disable=no-self-use self, tx_log: EthereumTxReceiptLog, transaction: EthereumTransaction, # pylint: disable=unused-argument decoded_events: List[HistoryBaseEntry], # pylint: disable=unused-argument all_logs: List[EthereumTxReceiptLog], # pylint: disable=unused-argument action_items: List[ActionItem], # pylint: disable=unused-argument ) -> Tuple[Optional[HistoryBaseEntry], Optional[ActionItem]]: if tx_log.topics[0] == DEPOSIT: return self._decode_deposit(tx_log, transaction, decoded_events, all_logs, action_items) # noqa: E501 if tx_log.topics[0] == ORDER_PLACEMENT: return self._decode_order_placement(tx_log, transaction, decoded_events, all_logs, action_items) # noqa: E501 if tx_log.topics[0] == WITHDRAW_REQUEST: return self._decode_withdraw_request(tx_log, transaction, decoded_events, all_logs, action_items) # noqa: E501 if tx_log.topics[0] == WITHDRAW: return self._decode_withdraw(tx_log, transaction, decoded_events, all_logs, action_items) # noqa: E501 return None, None def _decode_deposit( # pylint: disable=no-self-use self, tx_log: EthereumTxReceiptLog, transaction: EthereumTransaction, # pylint: disable=unused-argument decoded_events: List[HistoryBaseEntry], # pylint: disable=unused-argument all_logs: List[EthereumTxReceiptLog], # pylint: disable=unused-argument action_items: List[ActionItem], # pylint: disable=unused-argument ) -> Tuple[Optional[HistoryBaseEntry], Optional[ActionItem]]: topic_data, log_data = self.contract.decode_event( tx_log=tx_log, event_name='Deposit', argument_names=('user', 'token', 'amount', 'batchId'), ) deposited_asset = ethaddress_to_asset(topic_data[1]) if deposited_asset is None: return None, None amount = asset_normalized_value(amount=log_data[0], asset=deposited_asset) for event in decoded_events: # Find the transfer event which should come before the deposit if event.event_type == HistoryEventType.SPEND and event.asset == deposited_asset and event.balance.amount == amount and event.counterparty == self.contract.address: # noqa: E501 event.event_type = HistoryEventType.DEPOSIT event.event_subtype = HistoryEventSubType.DEPOSIT_ASSET event.counterparty = CPT_DXDAO_MESA event.notes = f'Deposit {amount} {deposited_asset.symbol} to DXDao mesa exchange' # noqa: E501 break return None, None def _decode_withdraw( # pylint: disable=no-self-use self, tx_log: EthereumTxReceiptLog, transaction: EthereumTransaction, # pylint: disable=unused-argument decoded_events: List[HistoryBaseEntry], # pylint: disable=unused-argument all_logs: List[EthereumTxReceiptLog], # pylint: disable=unused-argument action_items: List[ActionItem], # pylint: disable=unused-argument ) -> Tuple[Optional[HistoryBaseEntry], Optional[ActionItem]]: topic_data, log_data = self.contract.decode_event( tx_log=tx_log, event_name='Withdraw', argument_names=('user', 'token', 'amount'), ) withdraw_asset = ethaddress_to_asset(topic_data[1]) if withdraw_asset is None: return None, None amount = asset_normalized_value(amount=log_data[0], asset=withdraw_asset) for event in decoded_events: # Find the transfer event which should come before the withdraw if event.event_type == HistoryEventType.RECEIVE and event.asset == withdraw_asset and event.balance.amount == amount and event.counterparty == self.contract.address: # noqa: E501 event.event_type = HistoryEventType.WITHDRAWAL event.event_subtype = HistoryEventSubType.REMOVE_ASSET event.counterparty = CPT_DXDAO_MESA event.notes = f'Withdraw {amount} {withdraw_asset.symbol} from DXDao mesa exchange' # noqa: E501 break return None, None def _decode_withdraw_request( # pylint: disable=no-self-use self, tx_log: EthereumTxReceiptLog, transaction: EthereumTransaction, # pylint: disable=unused-argument decoded_events: List[HistoryBaseEntry], # pylint: disable=unused-argument all_logs: List[EthereumTxReceiptLog], # pylint: disable=unused-argument action_items: List[ActionItem], # pylint: disable=unused-argument ) -> Tuple[Optional[HistoryBaseEntry], Optional[ActionItem]]: topic_data, log_data = self.contract.decode_event( tx_log=tx_log, event_name='WithdrawRequest', argument_names=('user', 'token', 'amount', 'batchId'), ) user = topic_data[0] if not self.base.is_tracked(user): return None, None token = ethaddress_to_asset(topic_data[1]) if token is None: return None, None amount = asset_normalized_value(amount=log_data[0], asset=token) event = HistoryBaseEntry( event_identifier=transaction.tx_hash.hex(), sequence_index=self.base.get_sequence_index(tx_log), timestamp=ts_sec_to_ms(transaction.timestamp), location=Location.BLOCKCHAIN, location_label=user, # Asset means nothing here since the event is informational. TODO: Improve? asset=token, balance=Balance(amount=amount), notes= f'Request a withdrawal of {amount} {token.symbol} from DXDao Mesa', event_type=HistoryEventType.INFORMATIONAL, event_subtype=HistoryEventSubType.REMOVE_ASSET, counterparty=CPT_DXDAO_MESA, ) return event, None def _decode_order_placement( # pylint: disable=no-self-use self, tx_log: EthereumTxReceiptLog, transaction: EthereumTransaction, # pylint: disable=unused-argument decoded_events: List[HistoryBaseEntry], # pylint: disable=unused-argument all_logs: List[EthereumTxReceiptLog], # pylint: disable=unused-argument action_items: List[ActionItem], # pylint: disable=unused-argument ) -> Tuple[Optional[HistoryBaseEntry], Optional[ActionItem]]: """Some docs: https://docs.gnosis.io/protocol/docs/tutorial-limit-orders/""" topic_data, log_data = self.contract.decode_event( tx_log=tx_log, event_name='OrderPlacement', argument_names=('owner', 'index', 'buyToken', 'sellToken', 'validFrom', 'validUntil', 'priceNumerator', 'priceDenominator'), # noqa: E501 ) owner = topic_data[0] if not self.base.is_tracked(owner): return None, None result = multicall_specific( ethereum=self.ethereum, contract=self.contract, method_name='tokenIdToAddressMap', arguments=[[topic_data[1]], [topic_data[2]]], ) # The resulting addresses are non checksumed but they can be found in the DB buy_token = ethaddress_to_asset(result[0][0]) if buy_token is None: return None, None sell_token = ethaddress_to_asset(result[1][0]) if sell_token is None: return None, None buy_amount = asset_normalized_value(amount=log_data[3], asset=buy_token) sell_amount = asset_normalized_value(amount=log_data[4], asset=sell_token) event = HistoryBaseEntry( event_identifier=transaction.tx_hash.hex(), sequence_index=self.base.get_sequence_index(tx_log), timestamp=ts_sec_to_ms(transaction.timestamp), location=Location.BLOCKCHAIN, location_label=owner, # Asset means nothing here since the event is informational. TODO: Improve? asset=sell_token, balance=Balance(amount=sell_amount), notes= f'Place an order in DXDao Mesa to sell {sell_amount} {sell_token.symbol} for {buy_amount} {buy_token.symbol}', # noqa: E501 event_type=HistoryEventType.INFORMATIONAL, event_subtype=HistoryEventSubType.PLACE_ORDER, counterparty=CPT_DXDAO_MESA, ) return event, None # -- DecoderInterface methods def addresses_to_decoders( self) -> Dict[ChecksumEthAddress, Tuple[Any, ...]]: return { self.contract.address: (self._decode_events, ), # noqa: E501 } def counterparties(self) -> List[str]: return [CPT_DXDAO_MESA]