vault_report.vault_probes
Documentation for eth_defi.vault_report.vault_probes Python module.
Deterministic onchain and API facts for the vault investability check.
The investability check, see eth_defi.vault_report.vault_checks,
uses an LLM agent for judgement, but reads everything that code can read
reliably here first:
where a vault’s money is: for Morpho V1 the markets on its withdraw queue, for Euler Earn its strategies, with each market’s collateral token, oracle, loan-to-value and free liquidity;
how much a depositor could redeem right now: idle assets plus the liquidity the vault can pull from its markets or strategies, not only idle assets, see
eth_defi/erc_4626/vault_protocol/README-vault-redeemable.md;DEX liquidity of each collateral token, from DexScreener;
liquidity, utilisation and served withdrawals over the last 30 days, from the report’s vault price Parquet.
The facts also raise deterministic suspicion signals. A signal never excludes a vault by itself: it marks the vault for the agent to research. The thresholds are investigation triggers from the plan, not calibrated verdicts.
Supported probes: Morpho V1 (MetaMorpho), Morpho V2 (idle assets only, its
adapters are not read yet), Euler Earn, Euler EVK and 40acres. Other
protocols get unsupported.
Who uses the facts:
eth_defi.vault_report.vault_checkswrites them tovault-check-facts-N.jsonfor the agent, and the deterministic prescreen of the average yield charts uses only their signals;the agent and humans probe a single vault with
scripts/erc-4626/probe-vault-positions.py.
Design notes:
All onchain reads of one vault use the same block, the chain head at probe time, so positions and totals are consistent with each other. The facts describe the vault now, not at the report’s data date, which is what an exit liquidity judgement needs. Reading the head also works on Monad, whose nodes keep only recent state.
A failed read never aborts the report: it is recorded in
VaultFacts.errorsfor the agent to see, and the rest of the facts are still filled in where possible.Reads are plain
eth_callrequests throughcreate_multi_provider_web3()with theJSON_RPC_*environment variables, one vault per thread. With a few dozen candidates per round this is fast enough without Multicall.
Module Attributes
A position at least this large, as a share of the vault's assets, is checked for suspicious collateral; smaller positions cannot move a vault's yield or solvency much |
|
Collateral with less DEX liquidity than this, in US dollars, has no practical market: liquidators could not sell it, so the loans against it cannot be valued at the oracle price. |
|
Redeemable liquidity below this share of the vault's assets raises an exit liquidity signal |
|
Days the liquidity must have stayed low to raise the historical exit liquidity signal; long enough that a temporary full utilisation does not trigger it |
|
DexScreener chain slugs by chain id, see the |
|
Euler EVK functions not in the committed ABIs: |
|
Exceptions an individual contract read may raise when a contract does not implement a function: a revert, empty return data, an ABI decoding failure or a token without ERC-20 metadata. |
Functions
|
Summarise liquidity, utilisation and served withdrawals over recent days. |
|
Convert facts to plain JSON data. |
|
Read facts for all in-scope candidates in parallel. |
|
Read the largest DEX pool liquidity of a token from DexScreener. |
|
Read the strategies of an Euler Earn vault. |
|
Read the markets of a MetaMorpho V1 vault. |
|
Read a single-pool lending vault: Euler EVK ( |
|
Read the onchain facts of one vault. |
|
Deterministic suspicion signals for the agent to research. |
|
Pick the probe for a vault. |
Classes
One place a vault's assets are: a Morpho market, an Euler strategy or one of its collaterals. |
|
Deterministic facts about one candidate vault. |
- SUSPICIOUS_POSITION_SHARE: float = 0.1
A position at least this large, as a share of the vault’s assets, is checked for suspicious collateral; smaller positions cannot move a vault’s yield or solvency much
- MIN_COLLATERAL_DEX_LIQUIDITY_USD = 50000
Collateral with less DEX liquidity than this, in US dollars, has no practical market: liquidators could not sell it, so the loans against it cannot be valued at the oracle price. The skill and its thresholds must stay in sync with these constants.
- MIN_REDEEMABLE_SHARE: float = 0.01
Redeemable liquidity below this share of the vault’s assets raises an exit liquidity signal
- LOW_LIQUIDITY_DAYS = 14
Days the liquidity must have stayed low to raise the historical exit liquidity signal; long enough that a temporary full utilisation does not trigger it
- DEXSCREENER_CHAINS = {1: 'ethereum', 10: 'optimism', 56: 'bsc', 100: 'gnosischain', 130: 'unichain', 137: 'polygon', 143: 'monad', 146: 'sonic', 999: 'hyperevm', 8453: 'base', 9745: 'plasma', 42161: 'arbitrum', 43114: 'avalanche', 59144: 'linea', 80094: 'berachain', 747474: 'katana'}
DexScreener chain slugs by chain id, see the
chainIdvalues of the DexScreener API; chains missing here get no DEX liquidity figure
- EVK_ABI = [{'name': 'cash', 'type': 'function', 'stateMutability': 'view', 'inputs': [], 'outputs': [{'type': 'uint256'}]}, {'name': 'LTVList', 'type': 'function', 'stateMutability': 'view', 'inputs': [], 'outputs': [{'type': 'address[]'}]}]
Euler EVK functions not in the committed ABIs:
cash(), the assets not lent out, andLTVList(), the collateral vaults the EVK vault accepts. The ABI guide ineth_defi/abi/README.mdallows an inline fragment of at most two functions.
- CALL_ERRORS = (<class 'web3.exceptions.ContractLogicError'>, <class 'web3.exceptions.BadFunctionCallOutput'>, <class 'ValueError'>, <class 'eth_defi.token.TokenDetailError'>)
Exceptions an individual contract read may raise when a contract does not implement a function: a revert, empty return data, an ABI decoding failure or a token without ERC-20 metadata. Caught narrowly so a missing function on one contract is a recorded fact, not a crash.
- class Exposure
Bases:
objectOne place a vault’s assets are: a Morpho market, an Euler strategy or one of its collaterals.
Written to the facts file under
exposures; the agent starts its collateral research from these, andraise_signals()checks them.- kind: str
morpho_market,euler_strategy,euler_collateral, oridlefor a Morpho market without collateral, which curators use to hold idle assets
- assets: float
The vault’s assets in this position, in denomination token units; 0 for
euler_collateral, whose exposure is not known
Share of the vault’s total assets; for
euler_collateralthe share of the whole EVK vault, an upper bound
- redeemable: Optional[float]
Assets the vault could withdraw from this position now, in denomination token units
- collateral: Optional[eth_typing.evm.HexAddress]
Collateral token address, if any
- oracle: Optional[eth_typing.evm.HexAddress]
Price oracle address of the market
- collateral_underlying: Optional[eth_typing.evm.HexAddress]
When the collateral is itself an ERC-4626 vault share, the vault’s underlying asset; DEX liquidity is then measured for the underlying asset
- collateral_dex_liquidity_usd: Optional[float]
Largest DEX liquidity of the collateral, or of its underlying asset, in US dollars;
Nonewhen unknown
- __init__(market, kind, assets, share_of_assets, redeemable=None, utilisation=None, collateral=None, collateral_symbol=None, collateral_name=None, oracle=None, lltv=None, collateral_underlying=None, collateral_dex_liquidity_usd=None)
- Parameters
market (str) –
kind (str) –
assets (float) –
share_of_assets (float) –
collateral (Optional[eth_typing.evm.HexAddress]) –
oracle (Optional[eth_typing.evm.HexAddress]) –
collateral_underlying (Optional[eth_typing.evm.HexAddress]) –
- Return type
None
- class VaultFacts
Bases:
objectDeterministic facts about one candidate vault.
One entry of the facts file the agent reads. The agent cites the file, with
observed_at, as evidence, so the observation time must be the time of the reads.- probe: str
Probe used, e.g.
morpho_v1, orunsupported, seeselect_probe()
- observed_at: Optional[str]
When the facts were read, naive UTC ISO timestamp; liquidity evidence older than seven days before the data date is rejected, see
eth_defi.vault_report.vault_checks.read_check_decisions()
- idle_assets: Optional[float]
Assets held idle by the vault: its own denomination token balance, or the
cash()of an Euler EVK vault
- redeemable_assets: Optional[float]
Assets a depositor could redeem now: idle plus what the vault can pull from its positions
Redeemable assets as a share of total assets
- exposures: list[eth_defi.vault_report.vault_probes.Exposure]
Where the assets are
- history: dict
Liquidity and withdrawal history, see
calculate_liquidity_history()
- errors: list[str]
Reads that failed, or facts the probe cannot read; the agent must not take a missing figure as a good one
- __init__(vault_id, probe, block_number=None, observed_at=None, total_assets=None, idle_assets=None, redeemable_assets=None, redeemable_share=None, exposures=<factory>, history=<factory>, signals=<factory>, errors=<factory>)
- Parameters
vault_id (str) –
probe (str) –
exposures (list[eth_defi.vault_report.vault_probes.Exposure]) –
history (dict) –
- Return type
None
- fetch_dex_liquidity_usd(chain_id, token, timeout=20.0)
Read the largest DEX pool liquidity of a token from DexScreener.
Calls the public, keyless
GET /token-pairs/v1/{chainId}/{tokenAddress}endpoint, see the DexScreener API. It is rate limited per IP, which is why callers look each token up only once per run, seefetch_vault_facts().The largest single pool, not the sum over pools, is returned: it is what a liquidator could sell into in one place, and it is not inflated by many dust pools. The result separates “no market” (0, which can raise a signal) from “unknown” (
None, which never does), so an API outage cannot make a vault look suspicious.- Parameters
chain_id (int) – Chain of the token.
token (Union[eth_typing.evm.HexAddress, str]) – Token address.
timeout (float) – HTTP timeout in seconds.
- Returns
Largest pool liquidity in US dollars, 0 when the token has no pools,
Nonewhen the chain is not on DexScreener or the lookup failed.- Return type
- fetch_morpho_v1_facts(web3, vault_address, chain_id, block)
Read the markets of a MetaMorpho V1 vault.
For each market on the withdraw queue: the vault’s supply position, the market’s free liquidity, utilisation, collateral token, oracle and LLTV. The vault can redeem the smaller of its position and the market’s free liquidity from each market. The withdraw queue is read because a MetaMorpho vault can only pull assets from the markets on it.
Contract reads, all at
block:vault
MORPHO(),withdrawQueueLength()andwithdrawQueue(i);Morpho Blue
idToMarketParams(id)for the loan and collateral tokens, oracle and LLTV;market(id)for the total supply and borrow assets and supply shares;position(id, vault)for the vault’s supply shares.
The vault’s position is its supply shares converted at the market’s stored totals, without the interest accrued since the market’s last update, so it can be slightly low. Free liquidity is total supply minus total borrow. A market without a collateral token is an idle market and is reported with kind
idle.See the MetaMorpho documentation.
- Parameters
web3 (web3.main.Web3) – Web3 connection of the vault’s chain.
vault_address (eth_typing.evm.HexAddress) – Vault address.
chain_id (int) – Chain id of the vault.
block (int) – Block number to read at, so all reads are consistent.
- Returns
Exposures, total assets and idle assets, in denomination token units.
- Return type
tuple[list[eth_defi.vault_report.vault_probes.Exposure], float, float]
- fetch_euler_earn_facts(web3, vault_address, chain_id, block)
Read the strategies of an Euler Earn vault.
Each strategy is an ERC-4626 vault, usually an Euler EVK lending vault.
maxWithdrawFromStrategygives what the Earn vault can pull from it now.Contract reads, all at
block: the Earn vault’swithdrawQueueLength()andwithdrawQueue(i); each strategy’sbalanceOf(earn vault)andconvertToAssets()for the position; the Earn vault’smaxWithdrawFromStrategy(strategy), or the strategy’smaxWithdraw(earn vault)on versions without it, for the redeemable amount. For each EVK strategy, its collateral tokens are listed too, see_fetch_euler_collateral().See the Euler Earn documentation.
- Parameters
web3 (web3.main.Web3) – Web3 connection of the vault’s chain.
vault_address (eth_typing.evm.HexAddress) – Vault address.
chain_id (int) – Chain id of the vault.
block (int) – Block number to read at, so all reads are consistent.
- Returns
Exposures, total assets and idle assets, in denomination token units.
- Return type
tuple[list[eth_defi.vault_report.vault_probes.Exposure], float, float]
- fetch_simple_pool_facts(web3, vault_address, chain_id, block, cash_function)
Read a single-pool lending vault: Euler EVK (
cash()) or 40acres (idle balance).An Euler EVK vault’s redeemable liquidity is its
cash(), the assets not lent out; other single-pool vaults report their idle balance. Also used as a lower bound for Morpho V2 vaults, whose adapters are not probed yet.For a 40acres pool the free liquidity depends on loan repayments, so the current idle balance alone says little;
raise_signals()also uses its 14-day history from the price Parquet.- Parameters
web3 (web3.main.Web3) – Web3 connection of the vault’s chain.
vault_address (eth_typing.evm.HexAddress) – Vault address.
chain_id (int) – Chain id of the vault.
block (int) – Block number to read at, so all reads are consistent.
cash_function (bool) – Read redeemable liquidity with the EVK
cash()function and list the EVK collateral tokens.
- Returns
Exposures, total assets and redeemable assets in denomination token units: with
cash_function, the EVK’s collateral tokens at the whole vault’s share andcash(); otherwise no exposures and the idle balance.- Return type
tuple[list[eth_defi.vault_report.vault_probes.Exposure], float, float]
- select_probe(protocol_slug, features)
Pick the probe for a vault.
One protocol slug covers several contract families, so the scanner’s detected ERC-4626 features choose between them:
morpho_v2_likeseparates Morpho V2 from MetaMorpho V1, andeuler_earn_likeseparates Euler Earn aggregators from EVK lending pools.
- calculate_liquidity_history(prices_path, vault_ids, end_at, days=30)
Summarise liquidity, utilisation and served withdrawals over recent days.
The probes see one moment; this history shows whether low liquidity is lasting and whether depositors actually got out. TVL changes include deposits and returns, so withdrawals are measured from the scanner’s withdrawal counters instead.
Only the needed columns and rows are read: the Parquet filters push the vault ids and the window down to the reader, which matters for a file of several hundred megabytes. The hourly rows are resampled to the last value of each day.
idle_share_max_14dis the highest dailyavailable_liquidity / total_assetsover the lastLOW_LIQUIDITY_DAYSdays with data: if even the maximum is below the threshold, liquidity stayed low for the whole period.- Parameters
prices_path (pathlib.Path) – Vault price Parquet with
available_liquidity,utilisation,total_assets,daily_withdrawal_countanddaily_withdrawal_usdcolumns.end_at (datetime.datetime) – End of the window.
days (int) – Window length.
- Returns
Vault id -> summary with
days_with_data,idle_share_max_14d,utilisation_median,withdrawal_countandwithdrawal_usd; a figure isNonewhen its column has no data in the window. Vaults without rows in the window are missing.- Return type
- raise_signals(facts)
Deterministic suspicion signals for the agent to research.
Signals are triggers, never verdicts: the agent decides, and a known asset with another price source, such as a tokenised fund with a primary-market NAV, may have no DEX market and still be fine. In the prescreen of the average yield charts a vault with any signal is escalated to the agent, so a missing figure must not raise one.
The signals:
Illiquid collateral. A position of at least
SUSPICIOUS_POSITION_SHAREof the assets lent against a collateral token, or its underlying asset, with less thanMIN_COLLATERAL_DEX_LIQUIDITY_USDof DEX liquidity. Unknown DEX liquidity raises nothing. Duplicates are dropped, because an Euler Earn vault can reach the same collateral through several strategies.No redeemable liquidity now. Redeemable assets below
MIN_REDEEMABLE_SHAREof the total.Lasting low liquidity, 40acres only. Free liquidity below
MIN_REDEEMABLE_SHAREon every day of the lastLOW_LIQUIDITY_DAYSdays. For 40acres pools the price Parquet’s available liquidity is the idle balance, which is what a depositor can redeem. For Morpho and Euler Earn it is only the vault’s own idle balance, not what it can pull from its markets, seeeth_defi/erc_4626/vault_protocol/README-vault-redeemable.md, so it would raise false signals.
- Parameters
facts (eth_defi.vault_report.vault_probes.VaultFacts) – Facts with exposures and history filled in.
- Returns
Human-readable signals; empty when nothing looks suspicious.
- Return type
- fetch_vault_facts(vault_id, protocol_slug, features, dex_cache)
Read the onchain facts of one vault.
Runs the probe chosen by
select_probe()at the current chain head, then derives the vault-wide redeemable liquidity:Morpho V1: idle balance plus what each withdraw-queue market can return;
Euler Earn: idle balance plus what each strategy can return; the
euler_collateralexposures hold no assets of their own and are not added;Euler EVK:
cash(); 40acres: the idle balance;Morpho V2: unknown, recorded in
errors, because its adapters are not read yet.
Then each collateral token’s DEX liquidity is looked up on DexScreener. A collateral that is itself an ERC-4626 vault share, e.g. a Morpho vault token, has no DEX pools of its own, so its underlying asset is looked up instead. That read is at the latest block, not
block, which is harmless for a vault’s immutableasset().Signals are not raised here: they also need the liquidity history, which
fetch_candidate_facts()adds for all vaults at once.- Parameters
- Returns
Facts; failed reads are recorded in
errorsinstead of raising.- Return type
- fetch_candidate_facts(candidates, prices_path, end_at, max_workers=8)
Read facts for all in-scope candidates in parallel.
Called by the investability check for each agent round and for the prescreen of the average yield charts. The work is network-bound (JSON-RPC and DexScreener), so a joblib threading pool runs one vault per thread, with a tqdm progress bar so a slow RPC is visible in the log. After the probes, the liquidity history is read from the price Parquet in one pass for all vaults, and the signals are raised.
- Parameters
candidates (list[dict]) – Candidate records with
vault_id,protocol_slugandfeatures, e.g.CheckCandidateas a dict.prices_path (Optional[pathlib.Path]) – Vault price Parquet for the liquidity history, or
Noneto skip it.end_at (datetime.datetime) – Report data date.
max_workers (int) – Parallel threads,
MAX_WORKERSin the scripts.
- Returns
Vault id -> facts.
- Return type
- facts_to_json(facts)
Convert facts to plain JSON data.
- Parameters
facts (dict[str, eth_defi.vault_report.vault_probes.VaultFacts]) – Output of
fetch_candidate_facts().- Returns
Vault id -> fact dictionary.
- Return type