[codex] Gate JUSD Gateway deposits while Savings is disabled#266
Conversation
…guard Improvements on top of the issue-263 Phase 1 gating: - Telemetry: expose JuiceGatewayService.getMetrics() (blocked-deposit counters keyed by chain:routeShape + SavingsRateProbe gauge/failure counters) via the existing JSON /metrics endpoint. Closes the acceptance-criteria telemetry gap without adding a Prometheus dep. - SavingsRateProbe: true stale-while-revalidate — serve the cached rate immediately and refresh in a coalesced background probe, so request latency never pays for the RPC. Adds an idle() test hook. - Observability: warn-log the fail-open path when an integrated chain has no registered vault. - DRY: extract isGatewayDepositDisabledError()/sendGatewayDepositDisabled() into endpoints/gatewayDepositGuard.ts and drop the duplicated error blocks in quote/swap/lpCreate/lpIncrease. typecheck clean, eslint 0 errors, jest 73/73 green (+4 new tests).
Tester gap-analysis follow-ups: - gatewayDepositGuard.test.ts: isGatewayDepositDisabledError type guard (positive/negative/null) + sendGatewayDepositDisabled with and without error.message — closes the FALLBACK_DETAIL branch (now 100% coverage). - JuiceGatewayService: assert the deposit gate also fires on Citrea Testnet (5115), not just mainnet. jest 78/78 green.
- SavingsRateProbe.integration.test.ts: opt-in (RUN_CITREA_INTEGRATION=1) live check against Citrea Mainnet — probe reads currentRatePPM()==0, Savings.save reverts with ModuleDisabled() (0x6dff2fe8), JUSD->cBTC is gated while JUSD->CTUSD direct conversion stays open. Skipped by default so the unit CI matrix stays deterministic. Closes the issue-263 acceptance-criteria RPC integration gap. - Cover clearOverride() (per-chain and clear-all) — SavingsRateProbe rises to 95.6% line coverage; remaining uncovered lines are the default ethers factory (exercised by the integration test) and defensive no-vault/swallow branches. jest 80 passed, 3 skipped (integration), green.
Fixer-Architect (Big Brother loop) finding on PR JuiceSwapxyz#266: - Thundering herd: the cold-cache path of getCurrentRate() bypassed the inFlight coalescing, so N concurrent first-callers per chain each fired SAVINGS()+currentRatePPM(). Route both the cold path and the background SWR refresh through a single shared getOrStartRefresh() promise — a burst of startup requests now triggers exactly one probe. Regression test added. - clearOverride() now drops the override-seeded ratePpmByChain gauge entry so /metrics never reports a stale forced rate after the override lifts. jest 81 passed, 3 skipped; SavingsRateProbe branch coverage 89.5%.
Tester ↔ Fixer loop (3×) + reviewRan a 3-iteration tester↔fixer loop on a fresh clone of this branch, then a manual review. Verification (final state, all green)
Live Citrea Mainnet validationRan the gated integration suite (
Fix applied this round
Review notes (no blocking issues)
✅ LGTM after the formatting fix (pushed to this branch as |
Round-2 Fixer-Architect finding on PR JuiceSwapxyz#266: refresh() persisted the probed rate to cache+gauge unconditionally. A probe that resolved after setOverride() resurrected the pre-override rate, so /metrics contradicted the active override and (cache not cleared by clearOverride) a stale rate could be served within TTL after the override lifted. refresh() now skips the cache/gauge write while an override is active. Deterministic regression test added. jest green.
detectLpGatewayRouting/getInternalPoolToken were widened to JUSD || bridged stablecoin || JUICE, but the LP handlers (handleGatewayLpCreate/handleGatewayLpIncrease) only convert literal JUSD legs to svJUSD. Once Savings is re-enabled, a USDC/cBTC or JUICE/cBTC LP would route into the svJUSD pool with a raw, unconverted amount fed into Position.fromAmount*, producing wrong amount0Raw/ amount1Raw. The new deposit gate masks this only while disabled. Revert both routing functions to JUSD-only so non-JUSD LP legs fall back to standard (pre-existing) handling. Decouple the deposit gate from detectLpGatewayRouting: lpCreate/lpIncrease now always call rejectIfLpRouteRequiresJusdDepositDisabled (a no-op unless a JUSD/ bridged/JUICE deposit leg is present), so bridged/JUICE LPs are still blocked while the Savings rate is zero. Add regression tests asserting bridged/JUICE LP legs do not route through the Gateway and are never mapped to svJUSD.
Review — verified merge-ready ✅Full validation of head CI / quality (replicated locally + GitHub)
Logic & consistency
Functional — live against Citrea Mainnet (4114)Booted the branch and hit
Plus the opt-in integration suite ( Verdict: logic correct, consistent with the existing Gateway/config patterns, CI green, and the gate + all non-deposit routes verified end-to-end on mainnet. LGTM. |
Summary
Root cause
Savings.currentRatePPM() is 0 on Citrea Mainnet, so svJUSD deposits revert with ModuleDisabled(). Existing quote/swap paths still produced calldata for Gateway routes that must deposit into svJUSD.
Validation
Fixes #263