Skip to content

3Jane: redefine junior-buffer alert around at-risk credit coverage #323

Description

@spalen0

Summary

The 3Jane Junior Buffer Low alert is noisy because it measures the junior tranche against the entire borrow book and pages at a normal design-level ratio. We should:

  1. keep whole-book capitalization as a low-severity structural monitor;
  2. improve the Envio indexer so it exposes current borrower exposure and markdown data;
  3. measure active impairment against the loss waterfall:
    • Insurance Fund first,
    • sUSD3 junior tranche second,
    • USD3 senior tranche last.

This makes the actionable alerts answer:

  • Is impaired credit approaching or exceeding the Insurance Fund, putting sUSD3 at risk?
  • Is impaired credit approaching or exceeding the full first-loss stack, putting USD3 at risk?

Example of the current noisy alert:

🚨 ⚠️ 3Jane Junior Buffer Low
📊 sUSD3 buffer: 12.80% of deployed credit
💰 sUSD3 backing: $8.18M | Deployed: $63.92M
⚠️ First-loss coverage is thin — USD3 holders at higher risk

Why the current alert needs to change

The current check is:

sUSD3 backing / total deployed credit < 15% → HIGH

sUSD3 is designed to be a thin junior tranche, so a ratio around 10–15% is not by itself evidence that a loss event is developing. The ratio remains useful as a portfolio capitalization/leverage measure, but the current 15% HIGH threshold overlaps the protocol's normal operating design and can create emergency PRs during healthy operation.

There is also an existing check_usd3_oc monitor derived from the same whole-book ratio:

OC = deployed_credit / (deployed_credit - susd3_backing)

Its 111% and 106% thresholds correspond to sUSD3/deployed ratios of approximately 9.9% and 5.7%. Redesigning only check_junior_buffer would therefore leave another HIGH/CRITICAL structural alert capable of firing with no impaired borrowers. Both checks must be reconciled as part of this change.

Loss waterfall

For cryptonative credit losses, the documented waterfall is:

  1. Insurance Fund — provides first-loss capital through settlement;
  2. sUSD3 — absorbs losses beyond the Insurance Fund;
  3. USD3 — impaired after the Insurance Fund and sUSD3 are exhausted.

Define:

I = available Insurance Fund assets
J = current sUSD3 backing
L = impaired loss exposure

The waterfall-aligned coverage metrics are:

Insurance coverage = I / L
Senior protection  = (I + J) / L
  • sUSD3 begins taking loss when L > I.
  • USD3 begins taking loss when L > I + J.

If displayed, the junior tranche's remaining capacity after insurance is:

J / max(L - I, 0)

J / L alone is not the correct indicator for when sUSD3 begins taking losses because it ignores the Insurance Fund ahead of it.

Step 1 — Improve the Envio borrower exposure feed

The monitor currently receives repayment-cycle information such as amountDue and endingBalance. endingBalance is the borrower's balance at cycle close and is used by MorphoCredit for penalty calculations; it is not guaranteed to equal current outstanding exposure after subsequent repayments, borrowing, interest, or premium accrual.

Extend the 3Jane Envio entity/feed to expose authoritative current exposure data for every borrower-market row:

currentBorrowShares
currentBorrowAssets
currentMarkdown
exposureUpdatedBlock
exposureUpdatedTimestamp

Indexer requirements:

  • Update exposure state for Borrow, Repay, PremiumAccrued, BorrowerMarkdownUpdated, DefaultStarted, DefaultCleared, and AccountSettled.
  • Add BorrowerMarkdownUpdated and any other required events to the indexer configuration.
  • Set current exposure to zero on settlement.
  • Ensure partial repayments reduce current exposure.
  • Account for base interest and borrower-specific premium accrual rather than treating endingBalance as live debt.
  • Expose enough block/timestamp metadata for the monitor to reject stale or incomplete data.
  • Validate indexed borrower exposure against MorphoCredit positions at sampled blocks.
  • Reconcile the sum of indexed borrower exposure against aggregate on-chain deployed credit within a documented rounding/staleness tolerance.

The exact indexer implementation may use contract-state reads or indexed shares plus market state, but the GraphQL contract consumed by monitoring must expose a current asset-denominated exposure. The monitoring script should not reconstruct current exposure from endingBalance.

All borrower exposure values originate in waUSDC market units. The feed or monitor must convert them to USDC using the same waUSDC conversion rate/block used for deployed_credit and Insurance Fund valuation. Coverage calculations must never mix raw waUSDC with USDC.

Load-result semantics

Refactor the loader so these states are distinguishable:

BorrowerSnapshotLoadResult(
    snapshots=...,
    complete=True,
    indexed_block=...,
    indexed_timestamp=...,
    error=None,
)

The following are unavailable, not "zero impaired borrowers":

  • ENVIO_GRAPHQL_URL is unset;
  • any GraphQL page fails;
  • GraphQL returns errors or malformed data;
  • pagination is incomplete;
  • the indexed block/timestamp is stale;
  • required exposure fields are missing or invalid;
  • aggregate reconciliation falls outside tolerance.

On unavailable/stale data:

  • do not calculate coverage;
  • do not clear outstanding coverage-alert state;
  • do not update exposure baselines;
  • continue structural/on-chain monitors;
  • emit a deduplicated MEDIUM data-health alert (no emergency PR), rather than relying only on logs.

Only a complete, fresh result containing zero impaired borrowers may clear coverage alerts.

Step 2 — Aggregate impaired exposure

Keep raw and risk-weighted values separate:

gross_impaired_exposure    = Σ current_exposure_i
risk_weighted_exposure     = Σ current_exposure_i × weight(bucket_i)
confirmed_default_exposure = Σ current_exposure_i where status_i == Default

Starting severity weights:

Status/bucket Weight
Default 1.0
Delinquent, ≤1d to default 0.9
Delinquent, ≤3d 0.7
Delinquent, ≤7d 0.5
Delinquent, ≤14d 0.3
Delinquent, >14d 0.3

These weights are an explicitly provisional risk proxy. They must be module-level/configurable constants and should be recalibrated when historical cure and charge-off data is available.

Expose an aggregate such as:

AtRiskExposure(
    gross_impaired,
    risk_weighted,
    confirmed_default,
    recognized_markdown,
    delinquent_exposure,
    largest_borrower_exposure,
    largest_borrower_address,
    count,
)

Messages must label risk_weighted as a risk estimate, not as a realized loss. confirmed_default_exposure should drive claims that a tranche is directly exposed. Document whether recognized markdown is subtracted from remaining exposure or displayed separately; do not silently double-count already-recognized losses against already-reduced first-loss capital.

Step 3 — Waterfall-aligned coverage alerts

Compute both warning and confirmed-default coverage.

A. Insurance coverage / junior-loss risk

insurance_weighted_coverage = I / risk_weighted_exposure
insurance_default_coverage  = I / confirmed_default_exposure
Condition Severity
insurance_weighted_coverage < 2.0x HIGH — impaired-credit warning
insurance_default_coverage <= 1.0x HIGH — confirmed defaults exceed Insurance Fund; sUSD3 is exposed

B. Full first-loss stack / senior protection

senior_weighted_coverage = (I + J) / risk_weighted_exposure
senior_default_coverage  = (I + J) / confirmed_default_exposure
Condition Severity
senior_weighted_coverage < 1.5x HIGH — senior margin is thin
senior_default_coverage <= 1.0x CRITICAL — confirmed default exposure exceeds the full first-loss stack

HIGH and CRITICAL are intentionally both actionable in the current dispatch design. They open emergency-withdrawal/cap-reduction PRs, but those PRs require manual verification before execution. The distinction remains valuable for prioritization and escalation; no dispatch architecture change is required by this issue.

At exact exhaustion (coverage == 1.0x), use the breach tier because no loss-absorption margin remains.

Deduplication

Use separate cache keys for insurance and senior coverage. Prefer tier-aware deduplication with hysteresis:

  • alert on entry into a worse tier;
  • re-alert within a tier only after a material deterioration (for example ≥10%);
  • do not re-alert for insignificant waUSDC-rate or rounding movements;
  • re-arm only after recovery above the threshold plus a small hysteresis margin;
  • mark alert state only after send_alert() succeeds.

Step 4 — Consolidate the whole-book structural monitors

Replace/demote both check_junior_buffer and the current whole-book check_usd3_oc behavior.

Keep a structural capitalization monitor, for example:

junior subordination = J / deployed_credit
total first-loss capitalization = (I + J) / deployed_credit

This monitor should:

  • be LOW severity;
  • never claim that a current borrower loss is occurring;
  • alert on material deterioration from a clearly defined trailing baseline;
  • use the protocol's configured minimum sUSD3 backing requirement as the absolute floor where possible, rather than an unvalidated hard-coded 8%;
  • document the baseline window and reset behavior.

This preserves the useful portfolio-capital signal without opening emergency PRs merely because the protocol is operating near its designed tranche ratio.

Optional follow-ups

  • Concentration: compare the largest impaired borrower and largest total borrower exposure against I and I + J.
  • Velocity: alert when risk-weighted impaired exposure grows materially over multiple runs, with a minimum absolute-dollar threshold to avoid percentage noise near zero.
  • Accounting reconciliation: display recognized markdown, unreported USD3 NAV loss, and remaining impaired exposure separately.

Files/repositories to touch

Envio indexer repository

  • Extend ThreeJaneBorrowerMarket with current exposure, markdown, and freshness fields.
  • Add/update event handlers and configuration.
  • Add indexer tests for borrow, partial repay, premium accrual, markdown, default clearing, and settlement.
  • Deploy the updated schema/feed before enabling coverage alerts.

yearn/monitoring

  • protocols/3jane/main.py
    • consume the improved exposure fields;
    • add explicit complete/fresh/unavailable load semantics;
    • add AtRiskExposure;
    • add insurance/junior-loss and senior-protection checks;
    • consolidate/demote the existing junior-buffer and USD3 OC structural checks;
    • load borrower snapshots once and pass them to all borrower checks;
    • add new cache keys and tier-aware deduplication.
  • tests/test_3jane.py
    • add aggregation, coverage, data-health, conversion, reconciliation, tiering, and dedup tests.
  • protocols/3jane/README.md
    • document exposure provenance, weights, waterfall, coverage metrics, structural metric, and dispatch behavior.
  • monitoring.yaml
    • replace the existing Junior Buffer/OC descriptions and add the new coverage and data-health monitors.

Acceptance criteria

  • A normal 10–15% sUSD3/deployed ratio does not produce a HIGH/CRITICAL alert without impaired borrowers.
  • With a complete and fresh feed containing no impaired borrowers, coverage alerts remain silent and prior coverage state re-arms.
  • endingBalance is not used as current borrower exposure.
  • Partial repayments, additional borrows, interest/premium accrual, markdown, and settlement are reflected in indexed current exposure.
  • Borrower exposure, sUSD3 backing, Insurance Fund assets, and deployed credit use consistent USDC valuation.
  • Insurance coverage follows the waterfall: sUSD3 is exposed when confirmed default exposure exceeds the Insurance Fund.
  • Senior protection follows the waterfall: USD3 is directly exposed when confirmed default exposure equals or exceeds Insurance Fund plus sUSD3 capital.
  • HIGH and CRITICAL coverage alerts both dispatch PR creation; manual verification remains required before execution.
  • The old check_usd3_oc path cannot independently emit HIGH/CRITICAL solely from the whole-book structural ratio.
  • Envio outage, stale data, malformed data, or partial pagination cannot be interpreted as zero impaired exposure and cannot clear prior alert state.
  • Alert deduplication tolerates insignificant numerical movement, escalates on tier changes/material deterioration, and re-arms with hysteresis.
  • README.md and monitoring.yaml are updated.
  • Indexer tests, ruff, mypy, tests/test_3jane.py, and tests/test_monitoring_config.py pass.

Parameters requiring later calibration

  1. Risk weights by delinquency bucket.
  2. The 2.0x insurance-warning and 1.5x senior-warning thresholds.
  3. Indexer freshness and aggregate-reconciliation tolerances.
  4. Material-deterioration and recovery-hysteresis percentages.
  5. Treatment of recognized markdown when calculating remaining loss exposure.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions