fix(re-verify): oracle-independent buffer payout + settle NAV-collapse guard - #23
Merged
jayeshy14 merged 1 commit intoJul 20, 2026
Conversation
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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()computedminBuf = _bandWad(bufferMinBps)on the unconditional path, and_bandWad → vault.totalNav() → safeLeg.value() → pt.value(). So a Pendle-oracle outage revertstotalNav()and bricks even a buffer-coverable payout — directly contradicting the invariant documented on the function ("buffer-only fast path (nopt.value()read), so a Pendle-oracle outage cannot block a buffer-only payout").The existing
test_m1_bufferOnlyPath_isOracleFreepassed only becauseMockNavVault.totalNavis a fixed number that severs thetotalNav → leg → ptchain 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 byrebalanceBuffer, not a payout-safety invariant), and value PT best-effort. An outage now delivers the buffer portion instead of reverting.G2 —
settleEpochcan poison an epoch and permanently lock requests (new)settleEpochwroteepochNavPerShare[epoch] = navPerShare()with no guard.navPerShare()can be0whenshareholderNavhas collapsed to 0 while redeem shares are still outstanding (totalSupply > 0). But0is the "unsettled" sentinel forepochNavPerShare(and a divide-by-zero for the deposit-share mint). Settling then writes0, and every claim/view reads that epoch as unsettled and revertsEpochNotSettledforever — the requests are permanently locked. Not in the original audit.Fix: revert
NavCollapsedwhenprice == 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 revertsNavCollapsedat settle. Fails to revert (epoch silently poisoned) without the guard.Both verified as genuine regression tests.