Skip to content

fix(re-verify): oracle-independent buffer payout + settle NAV-collapse guard - #23

Merged
jayeshy14 merged 1 commit into
mainfrom
fix/buffer-oracle-independence-and-navcollapse-guard
Jul 20, 2026
Merged

fix(re-verify): oracle-independent buffer payout + settle NAV-collapse guard#23
jayeshy14 merged 1 commit into
mainfrom
fix/buffer-oracle-independence-and-navcollapse-guard

Conversation

@jayeshy14

Copy link
Copy Markdown
Owner

PR G — the clean-win robustness fixes surfaced by the post-remediation adversarial re-verification (a multi-agent pass that re-checked all 21 fixes and hunted the changed surface). These two are unambiguous bugs with proportionate fixes. The medium-severity design calls it also raised (M4 breaker economics, H6/L8 stale/par-anchored minOut, H1 mid-term fee basis, L5 fee-mint health-gating) are deferred pending a design decision and are not in this PR.

G1 — buffer-only payout is not actually oracle-independent (M1 residual)

SafeLegManager.provide() computed minBuf = _bandWad(bufferMinBps) on the unconditional path, and _bandWad → vault.totalNav() → safeLeg.value() → pt.value(). So a Pendle-oracle outage reverts totalNav() and bricks even a buffer-coverable payout — directly contradicting the invariant documented on the function ("buffer-only fast path (no pt.value() read), so a Pendle-oracle outage cannot block a buffer-only payout").

The existing test_m1_bufferOnlyPath_isOracleFree passed only because MockNavVault.totalNav is a fixed number that severs the totalNav → leg → pt chain that exists in production — false assurance.

Fix: compute the band through a totalNav-revert-resilient helper (fall back to a zero reserve band — the band is a maintenance reserve, restored later by rebalanceBuffer, not a payout-safety invariant), and value PT best-effort. An outage now delivers the buffer portion instead of reverting.

G2 — settleEpoch can poison an epoch and permanently lock requests (new)

settleEpoch wrote epochNavPerShare[epoch] = navPerShare() with no guard. navPerShare() can be 0 when shareholderNav has collapsed to 0 while redeem shares are still outstanding (totalSupply > 0). But 0 is the "unsettled" sentinel for epochNavPerShare (and a divide-by-zero for the deposit-share mint). Settling then writes 0, and every claim/view reads that epoch as unsettled and reverts EpochNotSettled forever — the requests are permanently locked. Not in the original audit.

Fix: revert NavCollapsed when price == 0. The epoch becomes settleable again if NAV recovers above 0, and holders keep their shares/requests meanwhile (fairer than crystallizing a 0 payout).

Tests (+2, 151 total)

  • test_g1_bufferPayoutSurvivesTotalNavOracleOutage — a production-shaped live-NAV source; the buffer payout survives a PT-oracle outage. Fails "oracle down" without the fix.
  • test_g2_settleRevertsWhenNavCollapsedToZero — a collapsed-NAV redeem epoch reverts NavCollapsed at settle. Fails to revert (epoch silently poisoned) without the guard.

Both verified as genuine regression tests.

…e guard

Two robustness bugs surfaced by the post-remediation adversarial re-verification.

G1 (M1 residual): SafeLegManager.provide() computed the min-buffer band via
_bandWad -> vault.totalNav() -> safeLeg.value() -> pt.value() on the UNCONDITIONAL
path, so a Pendle-oracle outage bricked even a buffer-coverable payout, despite
the "buffer-only fast path (no pt.value() read)" invariant documented on the
function. The existing test_m1_bufferOnlyPath_isOracleFree masked this because
its MockNavVault.totalNav is a fixed number that severs the leg->pt chain present
in production. Fix: compute the band via a totalNav-revert-resilient helper
(fall back to a zero reserve band, restored later by rebalanceBuffer) and value
PT best-effort, so an outage delivers the buffer portion instead of reverting.

G2 (new, permanent-lock edge bug not in the original audit): settleEpoch wrote
epochNavPerShare[epoch] = navPerShare() with no guard. navPerShare can be 0
(shareholderNav collapsed to 0 while redeem shares are still outstanding), and 0
is the "unsettled" sentinel for epochNavPerShare (and a divide-by-zero for the
deposit-share mint). Settling then poisons the epoch: every claim/view reads it
as unsettled and reverts EpochNotSettled forever, locking the requests. Fix:
revert NavCollapsed when price == 0; the epoch becomes settleable again if NAV
recovers above 0, and holders keep their shares/requests meanwhile.

Tests (+2, 151 total): a production-shaped live-NAV source proves the buffer
payout survives a PT-oracle outage (fails "oracle down" without the fix); a
collapsed-NAV redeem epoch reverts NavCollapsed at settle (does not revert
without the guard). Both verified as real regression tests.
@jayeshy14
jayeshy14 merged commit 0c2e26c into main Jul 20, 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