Skip to content

Start funded sell ramps that the client never started - #1394

Open
ebma wants to merge 7 commits into
stagingfrom
fix/api-recover-funded-sells
Open

ebma wants to merge 7 commits into
stagingfrom
fix/api-recover-funded-sells

Conversation

@ebma

@ebma ebma commented Sep 29, 2026 •

Copy link
Copy Markdown
Member

Summary

An EVM SELL user broadcasts the source transaction (e.g. the Squid swap that delivers USDC on Base to the ephemeral), reports its hash through /v1/ramp/update, and then the client calls /v1/ramp/start. Both routes refuse 15 minutes after registration. Ethereum receipt waits make "hash reported in time, start late or never" realistic, and nothing then advanced the ramp: the recovery worker excluded initial, the cleanup worker handles terminal ramps only, and the unhandled-payment worker is BRL-onramp only. The funds sat on the ephemeral indefinitely. Found while reviewing the gold app (#1388 follow-ups).

  • RampService.recoverFundedSellRamp starts such a ramp with the deadline check skipped, like the existing recoverPaidAveniaRamp. It requires a SELL, a non-domestic output currency and a reported squidRouterSwapHash or squidRouterNoPermitTransferHash (the Base-USDC direct-transfer variant). Every other start validation is unchanged, and FundEphemeral still verifies the reported hash on-chain against the issued blueprint before any platform spend.
  • The recovery worker selects those ramps once they are 16 minutes to 3 days old and starts them; everything else resumes as before. Public /ramp/start and /ramp/update stay strict.
  • The startup flow-version check keeps these ramps' flow versions registered through the same predicate (getFundedInitialSellRampWhere), so a deploy cannot remove a flow the worker still starts.
  • AlfredPay (domestic) SELLs are excluded: their hash is verified only after gas funding. Such rows, if any, are rejected with 409 each cycle until they age out (error log capped at 100 entries).
  • Security spec: block-flow-architecture.md, ramp-phase-flows.md, transaction-validation.md, ephemeral-accounts.md, RISK-REGISTER.md (RISK-005, RISK-016).

Late execution typically starts 16-21 minutes after registration; price drift is bounded by the Nabla hard minimum and the per-ramp subsidy caps (the presigned swap deadline is one week). Still manual: a user who broadcast after the deadline, so the hash was never reported. Gold now refuses to broadcast that late (#1388); the widget needs the same guard (separate task).

Test plan

  • brl-offramp-crosschain.scenario.test.ts: a ramp with a reported hash, created 17 min ago, is started by the worker and completes; the same ramp still gets 400 from public start/update; no hash, 14 min, 15.5 min and over 3 days are left alone; a tampered hash fails with no BRLA moved.
  • brl-offramp.scenario.test.ts: the direct-transfer variant (squidRouterNoPermitTransferHash) is started and completes.
  • Worker, service guard and compatibility-scope unit tests; mutation-checked (keeping the deadline, dropping the min age, the 3-day bound or the hash predicate each fail a test).
  • Full API suite (1943 pass), bun typecheck, bun verify.

No Slack alert when the worker auto-starts a ramp (decided).

🤖 Generated with Claude Code

…ource hash

A SELL ramp whose user broadcast the source transaction and reported its
hash inside the 15 minute window, but whose client never reached
/ramp/start, stays initial forever with the funds on the ephemeral. Add a
service-level start that skips only the deadline check and requires a
non-domestic SELL with a reported hash; FundEphemeral still verifies the
hash on-chain before any platform spend. The public start and update
routes stay strict.

Keep the flow versions of these rows registered through the startup
compatibility check, since the recovery worker will be able to start them.
An EVM SELL ramp whose user broadcast the source transaction and reported
its hash, but whose client never called /ramp/start within 15 minutes,
stayed initial forever with the funds on the ephemeral: the recovery
worker skipped initial ramps and the unhandled-payment worker only knows
Avenia tickets.

Each cycle the recovery worker now starts initial SELL ramps of this flow
variant with a reported swap or direct-transfer hash, once they are more
than a minute past the public deadline and less than three days old.
Failures are logged on the ramp and retried next cycle. Public
/ramp/start and /ramp/update stay strict, and FundEphemeral still
verifies the hash on-chain before any platform spend.
Record the one deadline bypass added for SELL ramps whose source hash was
reported: who may start them, why a bogus hash cannot cause platform
spend, how late execution is bounded, and the manual case that remains
(broadcast after the deadline or hash never reported).
@netlify

netlify Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for vortexfi ready!

Name Link
🔨 Latest commit 8c3bd44
🔍 Latest deploy log https://app.netlify.com/projects/vortexfi/deploys/6abe8eed920f000007000258
😎 Deploy Preview https://deploy-preview-1394--vortexfi.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify

netlify Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for vrtx-dashboard canceled.

Name Link
🔨 Latest commit 8c3bd44
🔍 Latest deploy log https://app.netlify.com/projects/vrtx-dashboard/deploys/6abe8eed25e6160008012ce2

@netlify

netlify Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for vortex-sandbox ready!

Name Link
🔨 Latest commit 8c3bd44
🔍 Latest deploy log https://app.netlify.com/projects/vortex-sandbox/deploys/6abe8eedc0ee9300096d2291
😎 Deploy Preview https://deploy-preview-1394--vortex-sandbox.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The recovery guard can admit unverified AssetHub ramps, and failed starts are misreported as successful.

Review effort: Balanced
Findings: 1 High severity · 1 Medium severity

Open (2)
What changed in this PR

Adds automatic recovery for funded SELL ramps that were never started.

Changes:

  • Adds worker-driven funded SELL recovery with flow compatibility checks.
  • Adds unit and corridor regression coverage.
  • Updates operational and security documentation.
File Description
docs/​security-spec/​RISK-REGISTER.md Updates recovery risks.
docs/​security-spec/​03-ramp-engine/​transaction-validation.md Documents deadline bypasses.
docs/​security-spec/​03-ramp-engine/​ramp-phase-flows.md Documents funded SELL recovery.
docs/​security-spec/​03-ramp-engine/​ephemeral-accounts.md Updates stuck-funds mitigation.
docs/​security-spec/​03-ramp-engine/​block-flow-architecture.md Extends compatibility scope.
docs/​operations-testing.md Records regression coverage.
apps/​api/​src/​tests/​corridors/​brl-offramp.scenario.test.ts Tests direct-transfer recovery.
apps/​api/​src/​tests/​corridors/​brl-offramp-crosschain.scenario.test.ts Tests recovery boundaries and validation.
apps/​api/​src/​api/​workers/​ramp-recovery.worker.ts Selects and starts funded SELL ramps.
apps/​api/​src/​api/​workers/​ramp-recovery.worker.test.ts Tests worker selection and retries.
apps/​api/​src/​api/​services/​ramp/​ramp.service.ts Adds the recovery start path.
apps/​api/​src/​api/​services/​ramp/​ramp.service.recover-funded-sell.test.ts Tests recovery guards.
apps/​api/​src/​api/​services/​ramp/​ramp.service.moonbeam-retirement.test.ts Covers Moonbeam retirement.
apps/​api/​src/​api/​services/​phases/​blocks/​core/​compatibility-scope.ts Retains recoverable flow versions.
apps/​api/​src/​api/​services/​phases/​blocks/​core/​compatibility-scope.test.ts Tests compatibility boundaries.

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +643 to +645
rampState.type !== RampDirection.SELL ||
isDomesticToken(quote.outputCurrency as FiatToken) ||
!(squidRouterSwapHash || squidRouterNoPermitTransferHash)
Comment on lines +104 to +105
if (state.currentPhase === "initial") {
await rampService.recoverFundedSellRamp(state.id);
ebma added 4 commits October 1, 2026 18:43
…nded-sells

# Conflicts:
#	docs/security-spec/03-ramp-engine/ramp-phase-flows.md
The worker reads Date.now() after the test's 'before' sample, so 'before - cutoff' is at most the minimum age, never at least it; the assertion failed whenever a millisecond ticked in between (CI saw 959999 vs 960000).
FundEphemeral verifies a reported Squid hash on-chain only for EVM sources; an AssetHub SELL takes its own branch and never checks that hash. Quote creation already rejects AssetHub BRL SELLs and EUR SELLs, but the guard should not rely on that.
Each attempt catches its own error and resolves, so Promise.allSettled marked every failure as fulfilled and the summary always logged zero failures.
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.

2 participants