From 869c92b4dea3c163646b3d89cfd38a81db523873 Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Sun, 23 Jul 2023 03:45:32 +0000 Subject: [PATCH] feat: balancer apy stuffs --- yearn/apy/__init__.py | 1 + yearn/apy/balancer/simple.py | 333 +++++++++++++++++++++++++++++++++++ yearn/apy/booster.py | 55 ++++++ yearn/apy/common.py | 39 +++- yearn/apy/curve/simple.py | 21 +-- yearn/apy/gauge.py | 69 ++++++++ yearn/v2/vaults.py | 13 +- 7 files changed, 511 insertions(+), 20 deletions(-) create mode 100644 yearn/apy/balancer/simple.py create mode 100644 yearn/apy/booster.py create mode 100644 yearn/apy/gauge.py 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..d86407461 --- /dev/null +++ b/yearn/apy/balancer/simple.py @@ -0,0 +1,333 @@ +import asyncio +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 async_lru import alru_cache +from brownie import chain +from y import ERC20, Contract, Network, magic +from y.datatypes import Address +from y.prices.dex.balancer.v2 import BalancerV2Pool, balancer +from y.time import closest_block_after_timestamp_async +from y.utils.dank_mids import dank_w3 + +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 + +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 + + +if balancer.vaults: + _get_pool = alru_cache(balancer.vaults[0].contract.getPool.coroutine) + get_pool = lambda poolId: BalancerV2Pool(_get_pool(poolId.hex())[0]) + + +@lru_cache(maxsize=None) +def is_aura_vault(vault: "Vault") -> bool: + return len(vault.strategies) == 1 and 'aura' in vault.strategies[0].name.lower() + +async def get_gauge(token) -> Contract: + gauges = await get_all_gauges() + return gauges[token] + +_ignore_gauges = ["SingleRecipientGauge", "ArbitrumRootGauge", "GnosisRootGauge", "OptimismRootGauge", "PolygonRootGauge", "PolygonZkEVMRootGauge"] + +@alru_cache +async def get_all_gauges() -> Dict[Address, Contract]: + gauge_controller = await Contract.coroutine(addresses[chain.id]['gauge_controller']) + num_gauges = await gauge_controller.n_gauges.coroutine() + gauges = await asyncio.gather(*[gauge_controller.gauges.coroutine(i) for i in range(num_gauges)]) + gauges = await asyncio.gather(*[Contract.coroutine(gauge) for gauge in gauges]) + gauges = [gauge for gauge in gauges if gauge._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} + +async 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 = await Contract.coroutine(vault.token.address) + + try: + gauge = await get_gauge(vault.token.address) + except KeyError as e: + raise ApyError('bal', 'gauge factory indicates no gauge exists') from e + + try: + gauge_inflation_rate = await gauge.inflation_rate.coroutine(block_identifier=now) + except AttributeError as e: + raise ApyError('bal', f'gauge {gauge} {str(e)[str(e).find("object"):]}') from e + + gauge_working_supply = await gauge.working_supply.coroutine(block_identifier=now) + if gauge_working_supply == 0: + raise ApyError('bal', 'gauge working supply is zero') + + gauge_controller = await Contract.coroutine(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 await calculate_simple( + vault, + Gauge(pool.address, pool, gauge, gauge_weight, gauge_inflation_rate, gauge_working_supply), + samples + ) + +async 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, (performance_fee, management_fee, keep_bal) = await asyncio.gather( + magic.get_price(gauge.lp_token, block=now, sync=False), + get_vault_fees(vault, block=now), + ) + + apr_data = await 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) + +async 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, booster, booster_boost = await asyncio.gather( + get_debt_ratio(vault, strategy), + Contract.coroutine(addresses[chain.id]['booster']), + gauge.calculate_boost.coroutine(MAX_BOOST, addresses[chain.id]['booster_voter'], block), + ) + booster_fee = get_booster_fee(booster, block) + + bal_price, aura_price = asyncio.gather( + magic.get_price(addresses[chain.id]['bal'], block=block, sync=False), + magic.get_price(addresses[chain.id]['aura'], block=block, sync=False), + ) + + rewards = await Contract.coroutine(strategy.rewardsContract()) + rewards_tvl = pool_token_price * await ERC20(rewards, asynchronous=True).total_supply_readable() + + reward_rate, scale = await asyncio.gather( + rewards.rewardRate.coroutine(), + ERC20(rewards, asynchronous=True).scale, + ) + bal_rewards_per_year = (reward_rate / scale) * 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, swap_fees_apr, bonus_rewards_apr = await asyncio.gather( + get_aura_emission_rate(block), + calculate_24hr_swap_fees_apr(gauge.pool, block), + get_bonus_rewards_apr(rewards, rewards_tvl), + ) + 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 + + 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 + ) + +async def get_bonus_rewards_apr(rewards, rewards_tvl, block=None): + result = 0 + for index in range(rewards.extraRewardsLength(block_identifier=block)): + extra_rewards = await Contract.coroutine(await rewards.extraRewards.coroutine(index)) + reward_token = extra_rewards + if hasattr(extra_rewards, 'rewardToken'): + reward_token = await Contract.coroutine(await extra_rewards.rewardToken.coroutine()) + + extra_reward_rate, reward_token_scale, reward_token_price = await asyncio.gather( + extra_rewards.rewardRate.coroutine(block_identifier=block), + ERC20(reward_token, asynchronous=True).scale, + magic.get_price(reward_token, block=block, sync=False), + ) + extra_rewards_per_year = (extra_reward_rate / reward_token_scale) * SECONDS_PER_YEAR + extra_rewards_per_year_usd = extra_rewards_per_year * reward_token_price + result += extra_rewards_per_year_usd / rewards_tvl + return result + +async 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 = await vault.strategies[0].strategy.keepBAL.coroutine(block_identifier=block) / 1e4 + else: + keep_bal = 0 + performance = await vault_contract.performanceFee.coroutine(block_identifier=block) / 1e4 if hasattr(vault_contract, "performanceFee") else 0 + management = await vault_contract.managementFee.coroutine(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 + +async def get_aura_emission_rate(block=None) -> float: + aura = await Contract.coroutine(addresses[chain.id]['aura']) + initial_mint, supply, max_supply = await asyncio.gather( + aura.INIT_MINT_AMOUNT.coroutine(), + aura.totalSupply.coroutine(block_identifier=block), + aura.EMISSIONS_MAX_SUPPLY.coroutine(), + ) + max_supply += initial_mint + + if supply <= max_supply: + total_cliffs, reduction_per_cliff, minter_minted = await asyncio.gather( + aura.totalCliffs.coroutine(block_identifier=block), + aura.reductionPerCliff.coroutine(block_identifier=block), + get_aura_minter_minted(block), + ) + 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 + +async 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 = await dank_w3.eth.get_storage_at(addresses[chain.id]['aura'], 7, block_identifier=block) + return int(hb.hex(), 16) + +async def get_debt_ratio(vault, strategy) -> float: + info = await vault.vault.strategies.coroutine(strategy) + return info[2] / 1e4 + +async def calculate_24hr_swap_fees_apr(pool: Contract, block=None): + if not block: block = await closest_block_after_timestamp_async(datetime.today(), True) + yesterday = await closest_block_after_timestamp_async((datetime.today() - timedelta(days=1)).timestamp(), True) + pool = BalancerV2Pool(pool, asynchronous=True) + swap_fees_now, swap_fees_yesterday, pool_tvl = await asyncio.gather( + get_total_swap_fees(pool.id, block), + get_total_swap_fees(pool.id, yesterday), + pool.get_tvl(block=block), + ) + swap_fees_delta = float(swap_fees_now) - float(swap_fees_yesterday) + return swap_fees_delta * 365 / float(pool_tvl) + +async def get_total_swap_fees(pool_id: bytes, block: int) -> int: + pool = get_pool(pool_id) + return await pool.contract.getRate.coroutine(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..5a811852a --- /dev/null +++ b/yearn/apy/booster.py @@ -0,0 +1,55 @@ +from time import time + +from y.time import get_block_timestamp + +from yearn.apy.common import SECONDS_PER_YEAR, get_reward_token_price +from yearn.utils import contract + + +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..303094897 100644 --- a/yearn/apy/curve/simple.py +++ b/yearn/apy/curve/simple.py @@ -3,10 +3,8 @@ import logging 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 @@ -24,11 +22,10 @@ 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 +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 @@ -40,15 +37,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 +177,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..f4801de5f --- /dev/null +++ b/yearn/apy/gauge.py @@ -0,0 +1,69 @@ +import logging +from dataclasses import dataclass +from time import time + +from brownie import ZERO_ADDRESS +from y import Contract +from y.time import get_block_timestamp + +from yearn.apy.common import SECONDS_PER_YEAR, get_reward_token_price +from yearn.apy.curve.rewards import rewards +from yearn.typing import Address + +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/v2/vaults.py b/yearn/v2/vaults.py index 930af17d6..fff8d3a0f 100644 --- a/yearn/v2/vaults.py +++ b/yearn/v2/vaults.py @@ -13,15 +13,13 @@ from semantic_version.base import Version from y import ERC20, Contract, Network, magic from y.exceptions import NodeNotSynced, PriceError, yPriceMagicError -from y.networks import Network -from y.prices import magic from y.utils.events import get_logs_asap from yearn.common import Tvl from yearn.decorators import sentry_catch_all, wait_or_exit_after 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 import balancer from yearn.special import Ygov from yearn.typing import Address from yearn.utils import run_in_thread, safe_views @@ -273,6 +271,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 await apy.balancer.simple(self, samples) elif Version(self.api_version) >= Version("0.3.2"): return await apy.v2.average(self, samples) else: @@ -299,6 +299,7 @@ async def tvl(self, block=None): @cached_property def _needs_curve_simple(self): + from yearn.prices.curve import curve # some curve vaults which should not be calculated with curve logic curve_simple_excludes = { Network.Arbitrum: [ @@ -310,3 +311,9 @@ 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: [], + }.get(chain.id, []) + return self.vault.address not in exclusions and balancer.selector.get_balancer_for_pool(self.token.address)