Skip to content

fix(L6/I2/I3): emergency-bound escape hatch, oracle prepare, param scale - #22

Merged
jayeshy14 merged 1 commit into
mainfrom
fix/emergency-liquidity-scale-cardinality
Jul 20, 2026
Merged

fix(L6/I2/I3): emergency-bound escape hatch, oracle prepare, param scale#22
jayeshy14 merged 1 commit into
mainfrom
fix/emergency-liquidity-scale-cardinality

Conversation

@jayeshy14

Copy link
Copy Markdown
Owner

Batch PR F of the CPPI vault audit remediation — the remaining low/informational tail (follows A–E: #17 revert-resilience, #18 oracle-robustness, #19 execution-mev, #20 floor-correctness, #21 request-hygiene). Closes L6, I2, I3.

L6 — emergency de-risk reverts under thin / attacker-thinned liquidity (Low)

The permissionless emergency de-risk does one exactInputSingle at 98.5% of oracle-fair with no admin knob. If a required sell moves both the 500 and 3000 pools past 1.5%, both tiers revert and rebalance() reverts.

Fix: guardian/owner-settable emergencySlippageBps that widens only the healthy-oracle emergency bound, hard-capped within [EMERGENCY_SLIPPAGE_BPS (150), EMERGENCY_DEGRADED_SLIPPAGE_BPS (1000)] and resettable to the tight default (0). The scheduled and degraded bounds are untouched. Widening is a deliberate trade of more single-swap sandwich exposure (L7) for guaranteed execution during a declared dislocation. The overstated "atomic" invariant comment is corrected.

Deliberately not done: full chunked/partial-fill execution. rebalance() is re-callable every block and self-heals as arbitrage re-aligns the pool, so chunking is disproportionate to a self-healing Low and carries its own leftover-token/accounting risk.

I2 — dead oracle cardinality self-heal (Informational)

_bindMarket issued increaseObservationsCardinalityNext then reverted OracleNotReady in the same tx on a cold oracle, rolling the bump back so it never persisted.

Fix: standalone, non-reverting prepareMarket(market) (owner-only; the underlying market call is itself permissionless) to warm a market's TWAP window ahead of the roll. _bindMarket comment clarified.

I3 — hardcoded USDC_SCALE = 1e12 (Informational)

The vault, safe leg and PT adapter all parameterize assetDecimals, but ExecutionModule baked in 1e12 — a latent footgun for any non-6-decimal redeployment.

Fix: derive assetScale and the dust floor from an assetDecimals constructor arg (public immutables). All six construction sites updated (6 for USDC).

Tests (+8, 149 total)

  • I2: prepareMarket persists the cardinality bump / the _bindMarket path rolls it back on a cold oracle / owner-only — with minimal Pendle mocks (test/PendlePTAdapterPrepare.t.sol, the first non-fork adapter coverage).
  • I3: assetScale/dustFloor derivation for 6/8/18 decimals and a >18 revert.
  • L6: a guardian-widened bound clears the exact 3%-cost thin-liquidity de-risk that reverts at 150bps (test_h6_tightBoundStillProtectsWhenHealthy is the paired revert case); reset restores the tight bound; setter access + range bounds.

Full suite green.

L6 (emergency de-risk reverts under thin/attacker-thinned liquidity): the
permissionless de-risk swaps once at 98.5% of oracle-fair with no admin knob;
a >1.5% pool dislocation reverts both fee tiers. Add a guardian/owner-settable
emergencySlippageBps that widens ONLY the healthy-oracle emergency bound, capped
within [EMERGENCY_SLIPPAGE_BPS, EMERGENCY_DEGRADED_SLIPPAGE_BPS] and resettable
to the tight default (0). The scheduled and degraded bounds are untouched, and
the widening is a deliberate trade of more single-swap sandwich exposure (L7)
for guaranteed execution during a declared dislocation. Correct the overstated
"atomic" invariant comment. Full chunked/partial-fill execution is deliberately
not added: rebalance() is re-callable every block and self-heals as arbitrage
re-aligns the pool, so it is disproportionate to a self-healing Low.

I2 (dead oracle cardinality self-heal): _bindMarket issued
increaseObservationsCardinalityNext then reverted OracleNotReady in the same tx
on a cold oracle, rolling the bump back so it never persisted. Add a standalone,
non-reverting prepareMarket(market) to warm a market's TWAP window ahead of the
roll; clarify the _bindMarket comment.

I3 (hardcoded USDC_SCALE = 1e12): the vault, safe leg and PT adapter all
parameterize assetDecimals but ExecutionModule baked in 1e12, a latent
redeployment footgun for any non-6-decimal asset. Derive assetScale and the
dust floor from an assetDecimals constructor arg (public immutables). Updated
all six construction sites (6 for USDC).

Tests (+8, 149 total): three prepareMarket cases with minimal Pendle mocks
(persists bump / bind rolls it back / owner-only); scale+dust-floor derivation
for 6/8/18 decimals and >18 revert; guardian-widened bound clears the thin-
liquidity de-risk that reverts at 150bps, reset restores the tight bound, plus
setter access/bounds.
@jayeshy14
jayeshy14 merged commit 491d6e8 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