Skip to content

feat(yearn): monitor small parent vault deposits - #345

Open
tapired wants to merge 3 commits into
mainfrom
codex/yearn-small-parent-deposits
Open

feat(yearn): monitor small parent vault deposits#345
tapired wants to merge 3 commits into
mainfrom
codex/yearn-small-parent-deposits

Conversation

@tapired

@tapired tapired commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

No description provided.

@tapired
tapired requested a review from spalen0 August 28, 2026 13:18
@spalen0

spalen0 commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator

Code review

Reviewed the full diff plus utils/store.py, utils/logger.py, utils/alert.py, utils/telegram.py, protocols/yearn/alert_large_flows.py and check_indexer_freshness.py, and queried Kong live to check the real vault population.


Correctness

🔴 alert_small_parent_flows.py:293 — silently drops events after any gap longer than the lookback

since_ts = 0 if persisted_cursor else now - lookback, and save_cursor only ever runs inside the per-event loop. A chain/flow pair that returns zero events never stores a cursor, so it stays on the 2-hour lookback indefinitely — for as long as that pair has never seen a single qualifying event.

So any run gap longer than 2h permanently drops every event in [last_run, now-7200), with no error and nothing in the logs. Realistic causes: VPS down, a crash, or a run_with_alert abort on an earlier chain (see the next two findings, both of which abort the run).

Fix: persist a floor cursor/timestamp even on an empty page — e.g. store the chain's latest indexed block, or a last_scanned_ts — so resumption never depends on an event having been seen.

kong.py:200 — one malformed Kong row kills the whole monitor, permanently

fetch_kong_parent_vaults raises KongRequestError when any single vault is missing asset/decimals, and nothing catches it, so run_with_alert aborts the run before the remaining chains are scanned — every run, for as long as Kong serves that row. The sibling fetch_kong_vaults (~line 121) deliberately continues past bad rows instead.

Kong is clean today (0 of 117 parent vaults missing asset metadata), which is exactly why this would only ever fire at the worst time. Log and skip.

alert_small_parent_flows.py:342 — an Envio outage floods the channel

gql_request sends a send_envio_error_message on every failure and nothing short-circuits: monitor_flow_type breaks its page loop, monitor_chain tries the next flow type, main the next chain. With the indexer down, one run emits up to 12 near-identical Telegram messages.

alert_large_flows sends one per run, and check_indexer_freshness has an explicit 6-hour cooldown for exactly this. Abort the run (or set an "envio is down" flag) after the first failure.

alert_small_parent_flows.py:276 — unbounded alert fan-out, no aggregation or cap

Every qualifying event is its own immediate send_alertsend_telegram_message; no dedupe, batching or rate limiting anywhere in that path. A page can hold 1000 events and the loop keeps paging.

On yvUSDC a "small" flow is a 0.01 USDC deposit — trivially spammable — so a single run can try to push hundreds of messages into a group Telegram rate-limits at ~20/min, wedging the run and burying every other yearn alert. Aggregate a run's hits into one message with a per-run cap and an "N more" tail.

Coverage gaps: two sets of live vaults are never monitored

  • kong.py:194isHidden excludes live vaults with real TVL. Filtering on isHidden in addition to isRetired drops 9 vaults that are hidden-but-not-retired, including yETH-Recovery on mainnet (0xd7a540ba3626c0aa66e7DB4088971d0CD64695B6) with ~$6.0M TVL. isHidden is a UI-visibility flag, not a liveness flag; a $6M vault being dust-probed is precisely what this monitor exists to catch. Filter on isRetired only (optionally plus TVL > 0).
  • Berachain (80094) has 2 active parent vaults (yBERA, yHONEY) that go unmonitored because Chain has no member for it.

alert_small_parent_flows.py:33 — a single raw threshold means wildly different things per vault

10_000 raw units is 1e-14 WETH on an 18-decimal vault (a genuine dust probe), 0.01 USDC on a 6-decimal one, but 0.0001 WBTC ≈ $11 on yvWBTC-1 (8 decimals). So ordinary small WBTC deposits will page as "small parent-vault deposit" while the same dollar amount in yvUSDC never will. asset_decimals is already fetched and used for display — scale the threshold per vault, or express it in USD like alert_large_flows.

alert_small_parent_flows.py:158AttributeError on a "data": null response

response.get("data", {}).get("events") — the {} default only applies when the key is absent. GraphQL servers routinely return {"data": null, ...}; if that arrives without an errors key (already handled above), this raises AttributeError: 'NoneType' object has no attribute 'get' and aborts the whole run rather than skipping. Use (response.get("data") or {}).

alert_small_parent_flows.py:387--log-level / SMALL_PARENT_FLOWS_LOG_LEVEL are no-ops

logging.basicConfig configures the root logger, but utils.logger.get_logger (line 41) attaches its own handler and sets propagate = False, taking its level from the LOG_LEVEL env var. So --log-level=DEBUG changes no output. Copied from alert_large_flows.py, where it works only because that file uses logging.getLogger("alert_large_flows") directly. Either drop the flag or apply it with logger.setLevel(...).

Not verified: the Envio endpoint returned HTTP 500 for every query during this review, so the schema field names/types (sender, owner, receiver, and the blockNumber comparison type) could not be checked against the live indexer. Worth a manual smoke run before merge.


Optimizations (nice-to-have)

Steady-state this is ~18 HTTP calls per run, almost always returning zero rows. It can be 2.

Kong: 6 requests → 1, cached (protocols/yearn/kong.py) — monitor_chain calls fetch_kong_parent_vaults(chain) once per chain. chainId is an optional argument on Kong's vaults query and is also selectable as a field, so vaults(v3: true, yearn: true, vaultType: 1) with no chainId returns all 117 parent vaults across every chain in a single request. The list changes on the order of weeks, so it should also go through utils/disk_cache.DiskCache with a multi-hour TTL.

Envio: up to 12 requests → 1 (alert_small_parent_flows.py:298) — one request per (chain, flow_type), each with its own 30s timeout. alert_large_flows.py:309 already has the pattern: deposits:/withdrawals: aliases in one document with chainId: { _in: $chainIds }. Per-chain cursors are still expressible — replace the two-branch _or with one branch pair per chain ({ chainId: {_eq: 1}, blockNumber: {_gt: ...} }, …).

Half the default chain list can never alert (alert_small_parent_flows.py:376) — live Kong data for active (non-retired, non-hidden) vaultType: 1 vaults: Mainnet 15, Katana 6, Base 2, Berachain 2, and zero on Optimism, Polygon and Arbitrum. Those three cost a Kong round-trip plus a logger.warning("No active parent vaults returned for %s") every run. Deriving the chain set from the single all-chain Kong call above drops them and picks up Berachain at the same time.

alert_small_parent_flows.py:310 — one SQLite connect+commit per event. save_cursorstore.state_set_connect() opens a fresh connection, runs PRAGMA journal_mode=WAL, re-checks schema, writes and commits — per event. A first-run backfill or a busy page does that 1000 times. Batch to once per page, or hold one connection open for the loop. (Tradeoff: a possible duplicate alert if the process dies mid-page.)

On cadence — no change proposed, just noting for later: nothing here needs to be hourly (already-mined historical events, no on-chain reads, persistent cursor), so a longer interval would be safe once the cursor-floor bug above is fixed. As written it wouldn't be, and --lookback-seconds would need to be ≥ 2× any new interval.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants