diff --git a/scripts/debug_apy.py b/scripts/debug_apy.py index 7531feb41..1381673cb 100644 --- a/scripts/debug_apy.py +++ b/scripts/debug_apy.py @@ -1,3 +1,6 @@ +import os +import sys +import time import logging import os import traceback @@ -8,12 +11,14 @@ def main(address): from yearn.apy.common import get_samples + start = time.perf_counter() from yearn.v2.registry import Registry from yearn.v2.vaults import Vault registry = Registry() vault = Vault.from_address(address) vault.registry = registry - print(await_awaitable(vault.apy(get_samples()))) + logger.info(f'apy {str(await_awaitable(vault.apy(get_samples())))}') + logger.info(f' ⏱️ {time.perf_counter() - start} seconds') def with_exception_handling(): address = os.getenv("DEBUG_ADDRESS", None) @@ -31,3 +36,6 @@ def with_exception_handling(): logger.info("*** Available variables for debugging ***") available_variables = [ k for k in locals().keys() if '__' not in k and 'pdb' not in k and 'self' != k and 'sys' != k ] logger.info(available_variables) + +if __name__ == '__main__': + globals()[sys.argv[1]]() diff --git a/yearn/apy/__init__.py b/yearn/apy/__init__.py index 47b08bc7f..5907adc1b 100644 --- a/yearn/apy/__init__.py +++ b/yearn/apy/__init__.py @@ -1,4 +1,5 @@ from yearn.apy import v1, v2, velo +from yearn.apy.balancer import simple as balancer from yearn.apy.common import (Apy, ApyBlocks, ApyError, ApyFees, ApyPoints, ApySamples, get_samples) from yearn.apy.curve import simple as curve diff --git a/yearn/apy/balancer/simple.py b/yearn/apy/balancer/simple.py new file mode 100644 index 000000000..fc859ac3b --- /dev/null +++ b/yearn/apy/balancer/simple.py @@ -0,0 +1,303 @@ +import logging +import os +from dataclasses import dataclass +from datetime import datetime, timedelta +from decimal import Decimal +from functools import lru_cache +from pprint import pformat +from typing import TYPE_CHECKING, Dict + +from brownie import chain, web3 +from y import Contract +from y.datatypes import Address +from y.prices.dex.balancer.v2 import BalancerV2Pool, balancer + +from yearn.apy.booster import get_booster_fee +from yearn.apy.common import (SECONDS_PER_YEAR, Apy, ApyBlocks, ApyError, + ApyFees, ApySamples) +from yearn.apy.gauge import Gauge +from yearn.debug import Debug +from yearn.networks import Network +from yearn.prices import magic +from yearn.utils import closest_block_after_timestamp, contract + +if TYPE_CHECKING: + from yearn.v2.vaults import Vault + +logger = logging.getLogger(__name__) + +@dataclass +class AuraAprData: + boost: float = 0 + bal_apr: float = 0 + aura_apr: float = 0 + swap_fees_apr: float = 0 + bonus_rewards_apr: float = 0 + gross_apr: float = 0 + net_apr: float = 0 + debt_ratio: float = 0 + +addresses = { + Network.Mainnet: { + 'gauge_factory': '0x4E7bBd911cf1EFa442BC1b2e9Ea01ffE785412EC', + 'gauge_controller': '0xC128468b7Ce63eA702C1f104D55A2566b13D3ABD', + 'voter': '0xc999dE72BFAFB936Cb399B94A8048D24a27eD1Ff', + 'bal': '0xba100000625a3754423978a60c9317c58a424e3D', + 'aura': '0xC0c293ce456fF0ED870ADd98a0828Dd4d2903DBF', + 'booster': '0xA57b8d98dAE62B26Ec3bcC4a365338157060B234', + 'booster_voter': '0xaF52695E1bB01A16D33D7194C28C42b10e0Dbec2', + } +} + +MAX_BOOST = 2.5 +COMPOUNDING = 52 + +_get_pool = lru_cache(balancer.vaults[0].contract.getPool) +get_pool = lambda poolId: BalancerV2Pool(_get_pool(poolId.hex())[0]) + +def is_aura_vault(vault: "Vault") -> bool: + return len(vault.strategies) == 1 and 'aura' in vault.strategies[0].name.lower() + +def get_gauge(token) -> Contract: + return get_all_gauges()[token] + +_ignore_gauges = ["SingleRecipientGauge", "ArbitrumRootGauge", "GnosisRootGauge", "OptimismRootGauge", "PolygonRootGauge", "PolygonZkEVMRootGauge"] + +@lru_cache +def get_all_gauges() -> Dict[Address, Contract]: + gauge_controller = contract(addresses[chain.id]['gauge_controller']) + gauges = [ + gauge for i in range(gauge_controller.n_gauges()) + if (gauge:=contract(gauge_controller.gauges(i)) )._name not in _ignore_gauges + ] + for gauge in gauges: + if not hasattr(gauge, 'lp_token'): + logger.warning(f'gauge {gauge} has no `lp_token` method') + gauges.remove(gauge) + return {gauge.lp_token(): gauge for gauge in gauges} + +def simple(vault, samples: ApySamples) -> Apy: + if chain.id != Network.Mainnet: + raise ApyError('bal', 'chain not supported') + if not is_aura_vault(vault): + raise ApyError('bal', 'vault not supported') + + now = samples.now + pool = contract(vault.token.address) + + try: + gauge = get_gauge(vault.token.address) + except KeyError as e: + raise ApyError('bal', 'gauge factory indicates no gauge exists') from e + + try: + gauge_inflation_rate = gauge.inflation_rate(block_identifier=now) + except AttributeError as e: + raise ApyError('bal', f'gauge {gauge} {str(e)[str(e).find("object"):]}') from e + + gauge_working_supply = gauge.working_supply(block_identifier=now) + if gauge_working_supply == 0: + raise ApyError('bal', 'gauge working supply is zero') + + gauge_controller = contract(addresses[chain.id]['gauge_controller']) + gauge_weight = gauge_controller.gauge_relative_weight.call(gauge.address, block_identifier=now) + + if os.getenv('DEBUG', None): + logger.info(pformat(Debug().collect_variables(locals()))) + + return calculate_simple( + vault, + Gauge(pool.address, pool, gauge, gauge_weight, gauge_inflation_rate, gauge_working_supply), + samples + ) + +def calculate_simple(vault, gauge: Gauge, samples: ApySamples) -> Apy: + if not vault: raise ApyError('bal', 'apy preview not supported') + + now = samples.now + pool_token_price = magic.get_price(gauge.lp_token, block=now) + performance_fee, management_fee, keep_bal = get_vault_fees(vault, block=now) + + apr_data = get_current_aura_apr( + vault, gauge, + pool_token_price, + block=now + ) + + gross_apr = apr_data.gross_apr * apr_data.debt_ratio + + net_booster_apr = apr_data.net_apr * (1 - performance_fee) - management_fee + net_booster_apy = float(Decimal(1 + (net_booster_apr / COMPOUNDING)) ** COMPOUNDING - 1) + net_apy = net_booster_apy + + fees = ApyFees( + performance=performance_fee, + management=management_fee, + keep_crv=keep_bal, + cvx_keep_crv=keep_bal + ) + + if os.getenv('DEBUG', None): + logger.info(pformat(Debug().collect_variables(locals()))) + + composite = { + "boost": apr_data.boost, + "bal_rewards_apr": apr_data.bal_apr, + "aura_rewards_apr": apr_data.aura_apr, + "swap_fees_apr": apr_data.swap_fees_apr, + "bonus_rewards_apr": apr_data.bonus_rewards_apr, + "aura_gross_apr": apr_data.gross_apr, + "aura_net_apr": apr_data.net_apr, + "booster_net_apr": net_booster_apr, + } + + try: # maybe this last arg should just be optional? + blocks = ApyBlocks( + samples.now, + samples.week_ago, + samples.month_ago, + vault.reports[0].block_number + ) + except IndexError: + blocks = None + + return Apy('aura', gross_apr, net_apy, fees, composite=composite, blocks=blocks) + +def get_current_aura_apr( + vault, gauge, + pool_token_price, + block=None +) -> AuraAprData: + """Calculate the current APR as opposed to projected APR like we do with CRV-CVX""" + strategy = vault.strategies[0].strategy + debt_ratio = get_debt_ratio(vault, strategy) + booster = contract(addresses[chain.id]['booster']) + booster_fee = get_booster_fee(booster, block) + booster_boost = gauge.calculate_boost(MAX_BOOST, addresses[chain.id]['booster_voter'], block) + + bal_price = magic.get_price(addresses[chain.id]['bal'], block=block) + aura_price = magic.get_price(addresses[chain.id]['aura'], block=block) + + rewards = contract(strategy.rewardsContract()) + rewards_tvl = pool_token_price * rewards.totalSupply() / 10**rewards.decimals() + + bal_rewards_per_year = (rewards.rewardRate() / 10**rewards.decimals()) * SECONDS_PER_YEAR + bal_rewards_per_year_usd = bal_rewards_per_year * bal_price + bal_rewards_apr = bal_rewards_per_year_usd / rewards_tvl + + aura_emission_rate = get_aura_emission_rate(block) + aura_rewards_per_year = bal_rewards_per_year * aura_emission_rate + aura_rewards_per_year_usd = aura_rewards_per_year * aura_price + aura_rewards_apr = aura_rewards_per_year_usd / rewards_tvl + + swap_fees_apr = calculate_24hr_swap_fees_apr(gauge.pool, block) + bonus_rewards_apr = get_bonus_rewards_apr(rewards, rewards_tvl) + + net_apr = ( + bal_rewards_apr + + aura_rewards_apr + + swap_fees_apr + + bonus_rewards_apr + ) + + gross_apr = ( + (bal_rewards_apr / (1 - booster_fee)) + + aura_rewards_apr + + swap_fees_apr + + bonus_rewards_apr + ) + + if os.getenv('DEBUG', None): + logger.info(pformat(Debug().collect_variables(locals()))) + + return AuraAprData( + booster_boost, + bal_rewards_apr, + aura_rewards_apr, + swap_fees_apr, + bonus_rewards_apr, + gross_apr, + net_apr, + debt_ratio + ) + +def get_bonus_rewards_apr(rewards, rewards_tvl, block=None): + result = 0 + for index in range(rewards.extraRewardsLength(block_identifier=block)): + extra_rewards = contract(rewards.extraRewards(index)) + reward_token = extra_rewards + if hasattr(extra_rewards, 'rewardToken'): + reward_token = contract(extra_rewards.rewardToken()) + + extra_rewards_per_year = (extra_rewards.rewardRate(block_identifier=block) / 10**reward_token.decimals()) * SECONDS_PER_YEAR + extra_rewards_per_year_usd = extra_rewards_per_year * magic.get_price(reward_token, block=block) + result += extra_rewards_per_year_usd / rewards_tvl + return result + +def get_vault_fees(vault, block=None): + if vault: + vault_contract = vault.vault + if len(vault.strategies) > 0 and hasattr(vault.strategies[0].strategy, 'keepBAL'): + keep_bal = vault.strategies[0].strategy.keepBAL(block_identifier=block) / 1e4 + else: + keep_bal = 0 + performance = vault_contract.performanceFee(block_identifier=block) / 1e4 if hasattr(vault_contract, "performanceFee") else 0 + management = vault_contract.managementFee(block_identifier=block) / 1e4 if hasattr(vault_contract, "managementFee") else 0 + + else: + # used for APY calculation previews + performance = 0.1 + management = 0 + keep_bal = 0 + + return performance, management, keep_bal + +def get_aura_emission_rate(block=None) -> float: + aura = contract(addresses[chain.id]['aura']) + initial_mint = aura.INIT_MINT_AMOUNT() + supply = aura.totalSupply(block_identifier=block) + max_supply = initial_mint + aura.EMISSIONS_MAX_SUPPLY() + + if supply <= max_supply: + total_cliffs = aura.totalCliffs() + minter_minted = get_aura_minter_minted(block) + reduction_per_cliff = aura.reductionPerCliff() + current_cliff = (supply - initial_mint - minter_minted) / reduction_per_cliff + reduction = 2.5 * (total_cliffs - current_cliff) + 700 + + if os.getenv('DEBUG', None): + logger.info(pformat(Debug().collect_variables(locals()))) + + return reduction / total_cliffs + else: + if os.getenv('DEBUG', None): + logger.info(pformat(Debug().collect_variables(locals()))) + + return 0 + +def get_aura_minter_minted(block=None) -> float: + """According to Aura's docs you should use the minterMinted field when calculating the + current aura emission rate. The minterMinted field is private in the contract though!? + So get it by storage slot""" + + # convert HexBytes to int + hb = web3.eth.get_storage_at(addresses[chain.id]['aura'], 7, block_identifier=block) + return int(hb.hex(), 16) + +def get_debt_ratio(vault, strategy) -> float: + return vault.vault.strategies(strategy)[2] / 1e4 + +def calculate_24hr_swap_fees_apr(pool: Contract, block=None): + if not block: block = closest_block_after_timestamp(datetime.today(), True) + yesterday = closest_block_after_timestamp((datetime.today() - timedelta(days=1)).timestamp(), True) + pool = BalancerV2Pool(pool) + swap_fees_now = get_total_swap_fees(pool.id, block) + swap_fees_yesterday = get_total_swap_fees(pool.id, yesterday) + swap_fees_delta = float(swap_fees_now) - float(swap_fees_yesterday) + pool_tvl = pool.get_tvl(block=block) + return swap_fees_delta * 365 / float(pool_tvl) + +def get_total_swap_fees(pool_id: bytes, block: int) -> int: + pool = get_pool(pool_id) + return pool.contract.getRate(block_identifier=block) / 10 ** 18 + \ No newline at end of file diff --git a/yearn/apy/booster.py b/yearn/apy/booster.py new file mode 100644 index 000000000..40c7a1d5b --- /dev/null +++ b/yearn/apy/booster.py @@ -0,0 +1,52 @@ +from time import time + +from yearn.apy.common import get_reward_token_price, SECONDS_PER_YEAR +from yearn.utils import contract, get_block_timestamp + +def get_booster_fee(booster, block=None) -> float: + """The fee % that the booster charges on yield.""" + lock_incentive = booster.lockIncentive(block_identifier=block) + staker_incentive = booster.stakerIncentive(block_identifier=block) + earmark_incentive = booster.earmarkIncentive(block_identifier=block) + platform_fee = booster.platformFee(block_identifier=block) + return (lock_incentive + staker_incentive + earmark_incentive + platform_fee) / 1e4 + +def get_booster_reward_apr( + strategy, + booster, + pool_price_per_share, + pool_token_price, + kp3r=None, rkp3r=None, + block=None +) -> float: + """The cumulative apr of all extra tokens that are emitted by depositing + to the booster, assuming they will be sold for profit. + """ + if hasattr(strategy, "id"): + # Convex hBTC strategy uses id rather than pid - 0x7Ed0d52C5944C7BF92feDC87FEC49D474ee133ce + pid = strategy.id() + else: + pid = strategy.pid() + + # get bonus rewards from rewards contract + # even though rewards are in different tokens, + # the pool info field is "crvRewards" for both convex and aura + rewards_contract = contract(booster.poolInfo(pid)['crvRewards']) + rewards_length = rewards_contract.extraRewardsLength() + current_time = time() if block is None else get_block_timestamp(block) + if rewards_length == 0: + return 0 + + total_apr = 0 + for x in range(rewards_length): + virtual_rewards_pool = contract(rewards_contract.extraRewards(x)) + if virtual_rewards_pool.periodFinish() > current_time: + reward_token = virtual_rewards_pool.rewardToken() + reward_token_price = get_reward_token_price(reward_token, kp3r, rkp3r, block) + reward_apr = ( + (virtual_rewards_pool.rewardRate() * SECONDS_PER_YEAR * reward_token_price) + / (pool_token_price * (pool_price_per_share / 1e18) * virtual_rewards_pool.totalSupply()) + ) + total_apr += reward_apr + + return total_apr diff --git a/yearn/apy/common.py b/yearn/apy/common.py index f12f1c0a3..f56a165c6 100644 --- a/yearn/apy/common.py +++ b/yearn/apy/common.py @@ -1,9 +1,8 @@ from dataclasses import dataclass from datetime import datetime, timedelta -from typing import Dict, Optional +from typing import Dict, Optional, Tuple -from brownie import web3 -from semantic_version.base import Version +from brownie import interface, web3 from y.time import closest_block_after_timestamp SECONDS_PER_YEAR = 31_556_952.0 @@ -86,3 +85,37 @@ def get_samples(now_time: Optional[datetime] = None) -> ApySamples: week_ago = closest_block_after_timestamp(int((now_time - timedelta(days=7)).timestamp()), True) month_ago = closest_block_after_timestamp(int((now_time - timedelta(days=31)).timestamp()), True) return ApySamples(now, week_ago, month_ago) + +def get_reward_token_price(reward_token, kp3r=None, rkp3r=None, block=None): + from yearn.prices import magic + + # if the reward token is rKP3R we need to calculate it's price in + # terms of KP3R after the discount + if str(reward_token) == rkp3r: + rKP3R_contract = interface.rKP3R(reward_token) + discount = rKP3R_contract.discount(block_identifier=block) + return magic.get_price(kp3r, block=block) * (100 - discount) / 100 + else: + return magic.get_price(reward_token, block=block) + +def calculate_pool_apy(vault, price_per_share_function, samples) -> Tuple[float, float]: + now_price = price_per_share_function(block_identifier=samples.now) + try: + week_ago_price = price_per_share_function(block_identifier=samples.week_ago) + except ValueError: + raise ApyError("common", "insufficient data") + + now_point = SharePricePoint(samples.now, now_price) + week_ago_point = SharePricePoint(samples.week_ago, week_ago_price) + + # FIXME: crvANKR's pool apy going crazy + if vault and vault.vault.address == "0xE625F5923303f1CE7A43ACFEFd11fd12f30DbcA4": + return 0, 0 + + # Curve USDT Pool yVault apr is way too high which fails the apy calculations with a OverflowError + elif vault and vault.vault.address == "0x28a5b95C101df3Ded0C0d9074DB80C438774B6a9": + return 0, 0 + + else: + pool_apr = calculate_roi(now_point, week_ago_point) + return pool_apr, (((pool_apr / 365) + 1) ** 365) - 1 diff --git a/yearn/apy/curve/simple.py b/yearn/apy/curve/simple.py index 5cf94960f..e50654bbb 100644 --- a/yearn/apy/curve/simple.py +++ b/yearn/apy/curve/simple.py @@ -4,9 +4,8 @@ import os from dataclasses import dataclass from decimal import Decimal -from pprint import pformat from functools import lru_cache - +from pprint import pformat from time import time import requests @@ -21,15 +20,15 @@ from y.time import get_block_timestamp_async from y.utils.dank_mids import dank_w3 +from yearn.apy.booster import get_booster_fee, get_booster_reward_apr from yearn.apy.common import (SECONDS_PER_WEEK, SECONDS_PER_YEAR, Apy, - ApyError, ApyFees, ApySamples, SharePricePoint, - calculate_roi) -from yearn.apy.curve.rewards import rewards + ApyError, ApyFees, ApySamples, + calculate_pool_apy, get_reward_token_price) +from yearn.apy.gauge import Gauge from yearn.apy.staking_rewards import get_staking_rewards_apr from yearn.debug import Debug from yearn.prices.curve import curve, curve_contracts -from yearn.typing import Address -from yearn.utils import contract +from yearn.utils import contract, get_block_timestamp @dataclass @@ -40,15 +39,6 @@ class ConvexDetailedApyData: cvx_debt_ratio: float = 0 convex_reward_apr: float = 0 -@dataclass -class Gauge: - lp_token: Address - pool: Contract - gauge: Contract - gauge_weight: int - gauge_inflation_rate: int - gauge_working_supply: int - logger = logging.getLogger(__name__) @@ -189,6 +179,11 @@ async def calculate_simple(vault, gauge: Gauge, samples: ApySamples) -> Apy: y_boost = y_working_balance / (PER_MAX_BOOST * y_gauge_balance) else: y_boost = BOOST[chain.id] + + # TODO figure out which is right + #base_apr = gauge.calculate_base_apr(MAX_BOOST, crv_price, pool_price, base_asset_price) + + #y_boost = gauge.calculate_boost(MAX_BOOST, addresses[chain.id]['yearn_voter_proxy'], block) # FIXME: The HBTC v1 vault is currently still earning yield, but it is no longer boosted. if vault and vault.vault.address == "0x46AFc2dfBd1ea0c0760CAD8262A5838e803A37e5": diff --git a/yearn/apy/gauge.py b/yearn/apy/gauge.py new file mode 100644 index 000000000..3956b73ce --- /dev/null +++ b/yearn/apy/gauge.py @@ -0,0 +1,67 @@ +import logging +from time import time +from dataclasses import dataclass +from brownie import Contract, interface, ZERO_ADDRESS + +from yearn.typing import Address +from yearn.utils import get_block_timestamp +from yearn.apy.common import get_reward_token_price, SECONDS_PER_YEAR +from yearn.apy.curve.rewards import rewards + +logger = logging.getLogger(__name__) + + +@dataclass +class Gauge: + lp_token: Address + pool: Contract + gauge: Contract + gauge_weight: int + gauge_inflation_rate: int + gauge_working_supply: int + + def calculate_base_apr(self, max_boost, reward_price, pool_price_per_share, pool_token_price) -> float: + return ( + self.gauge_inflation_rate + * self.gauge_weight + * (SECONDS_PER_YEAR / self.gauge_working_supply) + * ((1.0 / max_boost) / pool_price_per_share) + * reward_price + ) / pool_token_price + + def calculate_boost(self, max_boost, address, block=None) -> float: + balance = self.gauge.balanceOf(address, block_identifier=block) + working_balance = self.gauge.working_balances(address, block_identifier=block) + if balance > 0: + return working_balance / ((1.0 / max_boost) * balance) or 1 + else: + return max_boost + + def calculate_rewards_apr(self, pool_price_per_share, pool_token_price, kp3r=None, rkp3r=None, block=None) -> float: + if hasattr(self.gauge, "reward_contract"): + reward_address = self.gauge.reward_contract() + if reward_address != ZERO_ADDRESS: + return rewards(reward_address, pool_price_per_share, pool_token_price, block=block) + + elif hasattr(self.gauge, "reward_data"): # this is how new gauges, starting with MIM, show rewards + # get our token + # TODO: consider adding for loop with [gauge.reward_tokens(i) for i in range(gauge.reward_count())] for multiple rewards tokens + gauge_reward_token = self.gauge.reward_tokens(0) + if gauge_reward_token in [ZERO_ADDRESS]: + logger.warn(f"no reward token for gauge {str(self.gauge)}") + else: + reward_data = self.gauge.reward_data(gauge_reward_token) + rate = reward_data['rate'] + period_finish = reward_data['period_finish'] + total_supply = self.gauge.totalSupply() + token_price = get_reward_token_price(gauge_reward_token, kp3r, rkp3r) + current_time = time() if block is None else get_block_timestamp(block) + if period_finish < current_time: + return 0 + else: + return ( + (SECONDS_PER_YEAR * (rate / 1e18) * token_price) + / ((pool_price_per_share / 1e18) * (total_supply / 1e18) * pool_token_price) + ) + + return 0 diff --git a/yearn/prices/curve.py b/yearn/prices/curve.py index cb597f24f..4f0e06c87 100644 --- a/yearn/prices/curve.py +++ b/yearn/prices/curve.py @@ -14,6 +14,7 @@ """ import asyncio import logging +import os import threading import time from collections import defaultdict @@ -34,7 +35,7 @@ from yearn.exceptions import UnsupportedNetwork from yearn.multicall2 import fetch_multicall, fetch_multicall_async from yearn.typing import Address, AddressOrContract, Block -from yearn.utils import Singleton, contract, get_event_loop +from yearn.utils import Singleton, contract logger = logging.getLogger(__name__) @@ -126,7 +127,11 @@ def __init__(self) -> None: self._done = threading.Event() self._thread = threading.Thread(target=self.watch_events, daemon=True) self._has_exception = False - self._thread.start() + if not os.getenv('DISABLE_CURVE', False): + self._thread.start() + else: + self._done.set() + logger.warn('Curve exporter disabled') @sentry_catch_all def watch_events(self) -> None: @@ -416,6 +421,7 @@ def get_tvl(self, pool: AddressOrContract, block: Optional[Block] = None) -> flo pool = to_address(pool) balances = self.get_balances(pool, block=block) + from yearn.prices import magic return sum( amount * magic.get_price(coin, block=block) for coin, amount in balances.items() @@ -489,6 +495,7 @@ def get_coin_price(self, token: AddressOrContract, block: Optional[Block] = None token_out = contract(coins[token_out_ix]) amount_out = dy / 10 ** token_out.decimals() try: + from yearn.prices import magic return amount_out * magic.get_price(token_out, block = block) except PriceError: return None @@ -553,7 +560,7 @@ async def calculate_boost(self, gauge: Contract, addr: Address, block: Optional[ async def calculate_apy(self, gauge: Contract, lp_token: AddressOrContract, block: Optional[Block] = None) -> Dict[str,float]: pool = contract(self.get_pool(lp_token)) - results = fetch_multicall_async( + results = await fetch_multicall_async( [gauge, "working_supply"], [self.gauge_controller, "gauge_relative_weight", gauge], [gauge, "inflation_rate"], diff --git a/yearn/v2/vaults.py b/yearn/v2/vaults.py index 930af17d6..4ffbacf7e 100644 --- a/yearn/v2/vaults.py +++ b/yearn/v2/vaults.py @@ -22,6 +22,7 @@ from yearn.events import decode_logs, get_logs_asap from yearn.multicall2 import fetch_multicall_async from yearn.prices.curve import curve +from yearn.prices.balancer.balancer import selector as balancer_selector from yearn.special import Ygov from yearn.typing import Address from yearn.utils import run_in_thread, safe_views @@ -273,6 +274,8 @@ async def apy(self, samples: "ApySamples"): return await apy.curve.simple(self, samples) elif pool := await apy.velo.get_staking_pool(self.token.address): return await apy.velo.staking(self, pool, samples) + elif self._needs_balancer_simple(): + return apy.balancer.simple(self, samples) elif Version(self.api_version) >= Version("0.3.2"): return await apy.v2.average(self, samples) else: @@ -310,3 +313,15 @@ def _needs_curve_simple(self): needs_simple = self.vault.address not in curve_simple_excludes[chain.id] return needs_simple and curve and curve.get_pool(self.token.address) + + def _needs_balancer_simple(self): + exclusions = { + Network.Mainnet: [], + Network.Fantom: [], + Network.Arbitrum: [] + } + needs_simple = True + if chain.id in exclusions: + needs_simple = self.vault.address not in exclusions[chain.id] + + return needs_simple and balancer_selector.get_balancer_for_pool(self.token.address)