Skip to content

[codex] Gate JUSD Gateway deposits while Savings is disabled#266

Merged
Danswar merged 9 commits into
JuiceSwapxyz:developfrom
joshuakrueger-dfx:agent/fix-issue-263-jusd-gateway-gate
Jun 8, 2026
Merged

[codex] Gate JUSD Gateway deposits while Savings is disabled#266
Danswar merged 9 commits into
JuiceSwapxyz:developfrom
joshuakrueger-dfx:agent/fix-issue-263-jusd-gateway-gate

Conversation

@joshuakrueger-dfx

Copy link
Copy Markdown

Summary

  • add cached Savings rate probing with test override and fail-closed behavior for registered vault probe failures
  • block Gateway deposit routes for JUSD/bridged/JUICE inputs across quote, swap, GraphQL quote, and LP create/increase
  • prevent stale cached quotes from bypassing the deposit-disabled gate
  • add regression coverage for bridged stablecoins, GraphQL, stale cache, LP early gating, and probe failure cases

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

  • npm test -- --runInBand
  • npm run typecheck
  • npm run build
  • npm run lint (0 errors, existing warnings)
  • npx prettier --check src/**/*.ts
  • git diff --check && git diff --cached --check
  • tester/fixer loops with final tester pass reporting no findings

Fixes #263

@joshuakrueger-dfx joshuakrueger-dfx marked this pull request as ready for review May 29, 2026 15:00
…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%.
@joshuakrueger-dfx

Copy link
Copy Markdown
Author

Tester ↔ Fixer loop (3×) + review

Ran a 3-iteration tester↔fixer loop on a fresh clone of this branch, then a manual review.

Verification (final state, all green)

Check Result
tsc --noEmit ✅ pass
npm test --runInBand ✅ 81/81 (84/84 with RUN_CITREA_INTEGRATION=1)
eslint src ✅ 0 errors (212 pre-existing any warnings in statusFixers/*, untouched by this PR)
prettier --check "src/**/*.ts" ✅ pass (after fix below)
npm run build ✅ pass
git diff --check ✅ clean

Live Citrea Mainnet validation

Ran the gated integration suite (RUN_CITREA_INTEGRATION=1) against live RPC — confirms the PR's premise on-chain:

  • Savings.currentRatePPM() == 0
  • Savings.save(...) reverts with ModuleDisabled() (0x6dff2fe8) ✅
  • gate blocks JUSD → cBTC but allows JUSD → CTUSD against live state ✅

Fix applied this round

  • Prettier: 3 PR files (lpIncrease.ts, SavingsRateProbe.ts, SavingsRateProbe.test.ts) failed prettier --check with the recursive "src/**/*.ts" glob (the unquoted glob in the original validation likely didn't recurse). Reformatted — pure cosmetic, no logic change.

Review notes (no blocking issues)

  • Gate runs before the cache serve (quote.ts:325 vs :409) — stale cached quotes can't bypass it. ✅
  • Fail-open / fail-closed split is deliberate: unregistered vault → allow (can't prove zero, avoids bricking working chains); registered vault + probe failure + cold cache → block (Zero); probe failure + warm cache → serve stale. ✅
  • routeRequiresJusdDeposit correctly separates real svJUSD deposits from bridge/equity paths (verified live). ✅
  • inFlight coalescing avoids a cold-start RPC stampede; SWR keeps probe latency off the request path. ✅
  • All four guard call sites (quote / swap / GraphQL / LP) wired with correct try/catch around the shared gatewayDepositGuard. ✅
  • Minor/non-blocking: rate.toNumber() is safe (currentRatePPM is uint24); SWR honors a rate→0 flip only after ≤TTL (60s) — acceptable for a temporary disabled-module gate.

✅ LGTM after the formatting fix (pushed to this branch as fa61856).

joshuakrueger-dfx and others added 2 commits May 31, 2026 22:11
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.
@Danswar Danswar merged commit 09d60d2 into JuiceSwapxyz:develop Jun 8, 2026
7 checks passed
@joshuakrueger-dfx

Copy link
Copy Markdown
Author

Review — verified merge-ready ✅

Full validation of head b3fb0348 (incl. the LP-routing decouple commit). The PR's own GitHub checks are now green (Prettier, Quality Gates, linters, semgrep) and mergeState is CLEAN; below is independent verification.

CI / quality (replicated locally + GitHub)

  • npm run build (prisma + tsc) ✅ · tsc --noEmit
  • npm run test:unit85 passed, 3 opt-in skipped (live integration)
  • Prettier incl. .md ✅ 0 violations · ESLint ✅ 0 errors
  • semgrep ✅ 0 findings (110 rules, 73 files) · secret scan ✅ clean

Logic & consistency

  • Fail behavior is correct & asymmetric by design: no vault registered → fail-open (no savings deposit can revert); vault present but cold-probe fails → fail-closed (block rather than let the tx revert); probe error with cache → serve stale (SWR). A late probe cannot resurrect a pre-override rate.
  • Route classification is precise, not blanket: gates only (JUSD|bridged)→non-USD/JUICE and JUICE_IN→non-JUSD; never JUICE_OUT or svJUSD-in.
  • Gate at every deposit entry point: quote, swap, lpCreate, lpIncrease — and GraphQL via the shared createQuoteHandler. Uniform error handling through the shared gatewayDepositGuard.
  • b3fb034 fixes a latent bug: detectLpGatewayRouting/getInternalPoolToken narrowed to JUSD-only (the Gateway LP handlers only convert literal JUSD↔svJUSD; bridged/JUICE legs would otherwise be fed unconverted into svJUSD position math). Deposit-gate coverage decoupled and kept broad. Swap path untouched (separate detectRoutingType).
  • Reuses existing config/contracts helpers; vault registration wired so the gate is active in prod (not inert). quoteCache timer .unref() is a clean shutdown-hygiene fix.

Functional — live against Citrea Mainnet (4114)

Booted the branch and hit POST /v1/quote:

Route Result
JUSD → WcBTC 404 GATEWAY_DEPOSIT_DISABLED (was the on-chain revert in #263)
USDC → WcBTC 404 GATEWAY_DEPOSIT_DISABLED
WcBTC → JUSD 200, amountOut ≈ 469 JUSD
JUSD → CTUSD 200, amountOut ≈ 1 CTUSD (direct USD, not gated)
WcBTC → USDC 200, amountOut ≈ 469 USDC

Plus the opt-in integration suite (RUN_CITREA_INTEGRATION=1) passes 3/3: reads currentRatePPM()==0, confirms Savings.save() reverts with ModuleDisabled() (0x6dff2fe8), and gates JUSD→cBTC but not JUSD→CTUSD.

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.

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.

JUSD-input swaps revert on mainnet — Savings module disabled at 0% rate breaks Gateway

2 participants