Skip to content

feat(morpho-v2): one grouped governance alert per vault - #330

Merged
spalen0 merged 2 commits into
mainfrom
feat/morpho-v2-grouped-alerts
Jul 31, 2026
Merged

feat(morpho-v2): one grouped governance alert per vault#330
spalen0 merged 2 commits into
mainfrom
feat/morpho-v2-grouped-alerts

Conversation

@spalen0

@spalen0 spalen0 commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Supersedes #328, which was based on feat/peg-monitoring-l3-events and predates the pending-alert grouping already on main (8e09155) and the KATANA V2 vaults already in config.py. This branch is cut from main and keeps both.

1. One grouped alert per vault

Governance diffs fired one Telegram message per category, so a vault with new pending configs + a role change + an adapter swap produced several messages. Every category now buffers into a per-vault _VaultDiff, flushed as a single message with one header (V2 [name](url) on chain), one severity (highest of the group), and sections separated by ---.

main's collapsing of a shared validAt/txHash across a batched multicall is preserved — that batch stays one section rather than N.

Before (three messages, two vaults):

⚠️ ⏳ V2 Gauntlet USDT on KATANA
📥 Submitted: increaseTimelock(setSendAssetsGate → 604800s)
⏰ Executable at: 2026-08-06 19:11:34 (6 days)
🔗 Tx: 0x603be77d…

ℹ️ ✅ V2 Steakhouse Prime USDC on KATANA
Pending operation IncreaseAbsoluteCap() was executed (was due 2026-07-30 20:32:41).

ℹ️ ✅ V2 Steakhouse Prime USDC on KATANA
Pending operation IncreaseAbsoluteCap() was executed (was due 2026-07-30 20:32:41).

After (two messages — grouping is per vault, so the two Steakhouse resolutions merge):

⚠️ V2 [Gauntlet USDT](https://app.morpho.org/katana/vault/0xaC59…) on KATANA

📥 Submitted: increaseTimelock(setSendAssetsGate → 604800s)
⏰ Executable at: 2026-08-06 19:11:34 (6 days)
🔗 Tx: [0x603be77d…](https://katanascan.com/tx/0x603be77d…)
ℹ️ V2 [Steakhouse Prime USDC](https://app.morpho.org/katana/vault/0xbeef04…) on KATANA

✅ Pending operation `IncreaseAbsoluteCap()` was executed (was due 2026-07-30 20:32:41).

---

✅ Pending operation `IncreaseAbsoluteCap()` was executed (was due 2026-07-30 20:32:41).

The larger win is several categories landing on one vault in the same run — 4 messages become 1, at the highest severity of the group:

🚨 V2 [Gauntlet USDT](https://app.morpho.org/katana/vault/0xaC59…) on KATANA

📥 Submitted 2 operations:
  • increaseTimelock(setSendAssetsGate → 604800s)
  • increaseAbsoluteCap(market [vbUSDT/vbUSDC] → cap 5.00M vbUSDT)
⏰ Executable at: 2026-08-06 19:11:34 (6 days)
🔗 Tx: [0x603be77d…](https://katanascan.com/tx/0x603be77d…)

---

🚨 🎩 Curator changed: `0xabab…` → `0xcdcd…`

---

🧩 adapters changed
  + `0xDDdD…`
  − `0xEeee…`

2. Oversized groups split instead of truncating

utils.telegram truncates past MAX_MESSAGE_LENGTH and drops Markdown with it, so concatenating naively would lose the tail of exactly the large batches worth alerting on. _send_vault_alerts packs sections into as many (i/N) parts as needed, each under budget, with the header repeated. The budget derives from MAX_MESSAGE_LENGTH rather than a hardcoded number.

3. Cache cursors commit after the send

Previously the diff pass wrote cursors and then sent. If Telegram failed, the change was already marked as alerted and main() reported the failure without ever re-sending it — the alert was lost. Writes are now buffered in _VaultDiff and committed only once the send returns. A partial send (part 1 lands, part 2 fails) repeats the whole group next run; duplicates beat a governance change nobody ever sees.

Verification

Dry run against live data (LOG_LEVEL=DEBUG, isolated CACHE_DIR):

  • Sentora RLUSD Main — 4 pending cap increases in one message.
  • Gauntlet USDT on Katana — the increaseTimelock(setSendAssetsGate → 604800s) alert.
  • Second run: silent. No repeat alerts.

Tests

701 passed, ruff check and ruff format clean. New coverage:

  • every diff category collapses into one message at the group's highest severity
  • no diffs → no message
  • a LOW-only group stays LOW
  • an oversized group splits into numbered parts with no section dropped and every part ≤ 4096 chars
  • a raising send_alert leaves the cache untouched; a working one commits the cursors

🤖 Generated with Claude Code

Governance diffs fired one Telegram message per category, so a vault with
new pending configs plus an owner change plus an adapter swap produced five
separate messages. Every category now buffers into a per-vault _VaultDiff
that is flushed as a single message with one header and the highest severity
of the group. Pending submits keep their existing collapsing of a shared
validAt/txHash, so a batched multicall stays one section.

Two properties the buffer makes possible:

* Groups longer than Telegram's 4096-char cap split into "(i/N)" parts
  instead of being truncated — utils.telegram truncates and drops Markdown,
  which would silently lose the tail of exactly the large batches worth
  alerting on.
* Cache cursors are committed only after the send succeeds. Writing them
  during the diff pass marked a change as alerted even when Telegram failed,
  and main() reports the failure without ever re-sending it. A partial send
  now repeats the whole group next run; duplicates beat a governance change
  nobody ever sees.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@spalen0
spalen0 force-pushed the feat/morpho-v2-grouped-alerts branch from 364efbb to 3fb2601 Compare July 31, 2026 14:58
@spalen0 spalen0 changed the title feat(morpho-v2): one grouped governance alert per vault; add Yearn OG WBTC on Katana feat(morpho-v2): one grouped governance alert per vault Jul 31, 2026
@spalen0
spalen0 merged commit 9dbe544 into main Jul 31, 2026
3 checks passed
@spalen0
spalen0 deleted the feat/morpho-v2-grouped-alerts branch July 31, 2026 15:20
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.

1 participant