yearn / yearn-exporter

Realtime and historical Yearn metrics
https://yearn.vision
MIT License
95 stars 146 forks source link

Incorrectly computing Fee APY in Curve 3pool #482

Closed parkerburchett closed 1 year ago

parkerburchett commented 1 year ago

Backscratcher.apy(self, _: ApySamples) -> Apy at https://github.com/yearn/yearn-exporter/blob/master/yearn/special.py overestimates the APY from virtual price by looking back at APY from virtual price increases from the start of the pool instead of comparing the increase in virtual price from a year ago. This causes it to incorrectly measure APY from fees as ~2.3% instead of the APY from fees in the last year of ~.3%.

This is the function with the error

   def apy(self, _: ApySamples) -> Apy:
        curve_3_pool = contract("0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7")
        curve_reward_distribution = contract("0xA464e6DCda8AC41e03616F95f4BC98a13b8922Dc")
        curve_voting_escrow = contract("0x5f3b5DfEb7B28CDbD7FAba78963EE202a494e2A2")
        voter = "0xF147b8125d2ef93FB6965Db97D6746952a133934"
        crv_price = magic.get_price("0xD533a949740bb3306d119CC777fa900bA034cd52")
        yvecrv_price = magic.get_price("0xc5bDdf9843308380375a611c18B50Fb9341f502A")

        total_vecrv = curve_voting_escrow.totalSupply()
        yearn_vecrv = curve_voting_escrow.balanceOf(voter)
        vault_supply = self.vault.totalSupply()

        week = 7 * 86400
        epoch = math.floor(time() / week) * week - week
        tokens_per_week = curve_reward_distribution.tokens_per_week(epoch) / 1e18
        virtual_price = curve_3_pool.get_virtual_price() / 1e18
        # although we call this APY, this is actually APR since there is no compounding
        apy = (tokens_per_week * virtual_price * 52) / ((total_vecrv / 1e18) * crv_price)
        vault_boost = (yearn_vecrv / vault_supply) * (crv_price / yvecrv_price)
        composite = {
            "currentBoost": vault_boost,
            "boostedApy": apy * vault_boost,
            "totalApy": apy * vault_boost,
            "poolApy": apy,
            "baseApy": apy,
        }
        return Apy("backscratcher", apy, apy, ApyFees(), composite=composite)

    def tvl(self, block=None) -> Tvl:
        total_assets = self.vault.totalSupply(block_identifier=block)
        try:
            price = magic.get_price(self.token, block=block)
        except PriceError:
            price = None
        tvl = total_assets * price / 10 ** self.vault.decimals(block_identifier=block) if price else None
        return Tvl(total_assets, price, tvl)

This section in particular is wrong.

        virtual_price = curve_3_pool.get_virtual_price() / 1e18
        # although we call this APY, this is actually APR since there is no compounding
        apy = (tokens_per_week * virtual_price * 52) / ((total_vecrv / 1e18) * crv_price)

curve_3_pool.get_virtual_price() tracks the lp token value increase from swap fees from the start of the pool more than 2 years ago. The 3pool contract was deployed on Sep 6, 2020

To fix this you can update the function like this.

block_a_year_ago = some_function_to_get_the_block_nearest_to_a_year_ago()
previous_virtual_price = curve_3_pool.get_virtual_price(block_identifier=block_a_year_ago) / 1e18   
current_virtual_price = curve_3_pool.get_virtual_price() / 1e18
virtual_price_increase = 1 + (current_virtual_price - previous_virtual_price)
  # although we call this APY, this is actually APR since there is no compounding
  apy = (tokens_per_week * virtual_price_increase * 52) / ((total_vecrv / 1e18) * crv_price)

The way it is written now overestimates the APY by about 2%.

three_pool = eth_client.toChecksumAddress('0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7')
from analytics.abis import CURVE_STABLE_SWAP_ABI
contract = eth_client.eth.contract(three_pool, abi=CURVE_STABLE_SWAP_ABI)
contract.functions.get_virtual_price().call()

block_a_year_ago = 14194418
now_block = 16616116

previous_virtual_price = contract.functions.get_virtual_price().call(block_identifier=block_a_year_ago) / 1e18
current_virtual_price = contract.functions.get_virtual_price().call(block_identifier=now_block) / 1e18
virtual_price_increase = 1 + (current_virtual_price - previous_virtual_price)
previous_virtual_price, current_virtual_price, virtual_price_increase
$ (1.0203125062827059, 1.0237601567870114, 1.0034476505043055)

The virtual price only increases by ~0.34% in the last year instead of 2.37% since the lifetime of the pool.

DarkGhost7 commented 1 year ago

The backscratcher vault has been sunset and is not earning yield anymore atm, and it is encouraged to move to the new and improved yCRV wrapper and its derivatives st-ycrv or lp-ycrv to earn yield on crv at yearn. Thus this wont be fixed but fix will be noted so the code isnt duplicated elsewhere