ERC-4626: historical APY of a vault

Here is a Python example how to estimate the ERC-4626 APY.

  • Takes a time range and estimates the APY for it

  • The APY is calculated using the price difference of the vault share token

  • Uses FindBlock to convert timestamps to the historical block range.

  • Uses EVM JSON-RPC and archive node, no external services needed. Public RPC nodes won’t work, because they are not archive nodes. Get your Base node JSON-RPC access from dRPC or Ethereumnodes.com.

  • Supported vaults include all ERC-4626, including but not limited to: Morpho, Euler, Lagoon Finance, Superform, IPOR, Yearn, Fluid

Then to run this script:

# Get JSON-RPC archive node
export JSON_RPC_BASE=...
python scripts/erc-4626/read-historical-apy.py

Output looks like:

Vault: IPOR USDC Lending Optimizer Base (0x45aa96f0b3188d47a1dafdbefce1db6b37f58216)
Chain: Base
Estimated APY: 5.32%
Period: 2025-02-28 23:59:59 - 2025-04-30 23:59:59 (61 days)
Block range: 26,998,926 - 29,634,126
Share price at begin: 1.030367113833284958570710981 ipUSDCfusion / USDC
Share price at end: 1.039527386826594071396200349 ipUSDCfusion / USDC
Share price diff: 0.009160272993309112825489368 ipUSDCfusion / USDC

Further reading

"""An example script to estimate the historical APY of an ERC-4626 vault.

- Archive JSON-RPC node needed, public endpoint may not work.

To run:

.. code-block:: shell

    python scripts/erc-4626/read-historical-apy.py
"""

import os
import datetime

from eth_defi.chain import get_chain_name
from eth_defi.erc_4626.classification import create_vault_instance
from eth_defi.erc_4626.profit_and_loss import estimate_4626_recent_profitability, estimate_4626_profitability
from eth_defi.provider.multi_provider import create_multi_provider_web3
from eth_defi.timestamp import estimate_block_number_for_timestamp_by_findblock


def main():
    JSON_RPC_BASE = os.environ.get("JSON_RPC_BASE")
    assert JSON_RPC_BASE, "Please set JSON_RPC_BASE environment variable to your JSON-RPC endpoint."

    web3 = create_multi_provider_web3(JSON_RPC_BASE)
    chain_id = web3.eth.chain_id

    assert chain_id == 8453, "This script is designed to run on Base chain (chain ID 8453)."

    # IPOR USDC Base
    # Lending Optimizer
    # https://app.ipor.io/fusion/base/0x45aa96f0b3188d47a1dafdbefce1db6b37f58216
    vault_address = "0x45aa96f0b3188d47a1dafdbefce1db6b37f58216"
    vault = create_vault_instance(web3, vault_address)

    start_at = datetime.datetime(2025, 3, 1, tzinfo=None)
    end_at = datetime.datetime(2025, 5, 1, tzinfo=None)

    # Use FindBlock.xyz to estimate block numbers for the given timestamps
    start_block_find = estimate_block_number_for_timestamp_by_findblock(chain_id, start_at)
    end_block_find = estimate_block_number_for_timestamp_by_findblock(chain_id, end_at)

    profitability_data = estimate_4626_profitability(
        vault,
        start_block=start_block_find.block_number,
        end_block=end_block_find.block_number,
    )
    estimated_apy = profitability_data.calculate_profitability(annualise=True)
    start_block, end_block = profitability_data.get_block_range()
    start_at, end_at = profitability_data.get_time_range()
    start_price, end_price = profitability_data.get_share_price_range()
    diff = end_price - start_price

    print(f"Vault: {vault.name} ({vault_address})")
    print(f"Chain: {get_chain_name(chain_id)}")
    print(f"Estimated APY: {estimated_apy:.2%}")
    print(f"Period: {start_at} - {end_at} ({(end_at - start_at).days} days)")
    print(f"Block range: {start_block:,} - {end_block:,}")
    print(f"Share price at begin: {start_price} {vault.share_token.symbol} / {vault.denomination_token.symbol}")
    print(f"Share price at end: {end_price} {vault.share_token.symbol} / {vault.denomination_token.symbol}")
    print(f"Share price diff: {diff} {vault.share_token.symbol} / {vault.denomination_token.symbol}")


if __name__ == "__main__":
    main()