Skip to content

feat(vault): constituent quarantine and graceful redemption (Section 4)#14

Merged
jayeshy14 merged 1 commit into
mainfrom
feat/safety-quarantine
Jun 27, 2026
Merged

feat(vault): constituent quarantine and graceful redemption (Section 4)#14
jayeshy14 merged 1 commit into
mainfrom
feat/safety-quarantine

Conversation

@jayeshy14

Copy link
Copy Markdown
Owner

First slice of the Section 4 safety layer, the seam the constituent-governance work (PR #10) explicitly deferred and that Sections 16.5, 17.3, and invariant X2 depend on.

The problem

A single stale constituent feed halted NAV and therefore every redemption. That liveness cliff worsens at index-100, where the probability that at least one of N feeds is stale at any moment is not small. The vault now degrades instead of halting.

What changed

  • AssetRegistry: a non-reverting getPriceUsdStatus(token) that surfaces a stale feed's last-good answer, its updatedAt, and freshness, instead of reverting. The feed normalization is factored out so the reverting (getPriceUsd) and non-reverting paths share one implementation.
  • NAV degradation: a fresh constituent values at full price; a stale one values at its last-good price times a mark factor that starts at 1 - haircut and decays linearly to zero over quarantineDecayWindow; a dead feed (non-positive answer) values at zero. totalAssets and getHoldings degrade together and stay consistent. The USDC numeraire feed deliberately stays fail-closed, since it is the unit NAV is quoted in, not a constituent.
  • Exits bend, entries stop: sync buffer redemptions survive a stale feed at the haircut NAV, while mint, sync deposit, and the whole async settle fail closed under quarantine, because minting against a conservative NAV would over-issue shares and dilute the holders who stay. A stale feed on a constituent the vault does not actually hold does not block entries.
  • Quarantine params (quarantineHaircutBps default 10%, quarantineDecayWindow default 7 days) are owner-set.

Deferred to later slices

  • The NAV-per-share circuit breaker plus a vault pause and guardian role (the BoringVault-derived backstop for correlated mispricing).
  • The rebalancer dropping quarantined names from target weights so rebalancing continues around them. For now rebalancing still fails closed on a stale feed, which is the designed Section 17.2 behavior, so large exits are delayed but never lost.

Tests

10 new tests in test/IndexVaultQuarantine.t.sol: NAV degrades instead of reverting (exact haircut value asserted), dead feed marks to zero, decay to zero over the window, the headline buffer-redeem-survives-a-stale-feed guarantee, mint and settle fail closed, a zero-balance stale constituent does not block entries, and the param validation. 149 total, all green, forge fmt clean, no regression.

A single stale constituent feed previously halted NAV and therefore every
redemption, a liveness cliff that worsens at index-100 where 'all feeds fresh'
is fragile. The vault now degrades instead of halting.

- AssetRegistry: add a non-reverting getPriceUsdStatus that surfaces a stale
  feed's last-good answer and freshness, with the feed normalization factored
  out so the reverting and non-reverting paths share one implementation.
- NAV degradation: a fresh constituent values at full price; a stale one at its
  last-good price times a mark factor that starts at (1 - haircut) and decays
  linearly to zero over a window; a dead feed values at zero. totalAssets and
  getHoldings degrade together; the USDC numeraire feed stays fail-closed.
- Exits bend, entries stop: sync buffer redemptions survive a stale feed at the
  haircut NAV, while mint, sync deposit, and async settle fail closed under
  quarantine, since minting against a conservative NAV would over-issue and
  dilute the holders who stay. A zero-balance stale constituent does not block.

Deferred to later slices: the NAV-per-share circuit breaker and guardian pause,
and the rebalancer dropping quarantined names from target weights. 10 new
tests, 149 total green.
@jayeshy14 jayeshy14 merged commit a2cc212 into main Jun 27, 2026
3 checks passed
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