Conversation
…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).
✅ Deploy Preview for vortexfi ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for vrtx-dashboard canceled.
|
✅ Deploy Preview for vortex-sandbox ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
6 of 9 tasks
Contributor
There was a problem hiding this comment.
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
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); |
…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.
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.


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 excludedinitial, 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.recoverFundedSellRampstarts such a ramp with the deadline check skipped, like the existingrecoverPaidAveniaRamp. It requires a SELL, a non-domestic output currency and a reportedsquidRouterSwapHashorsquidRouterNoPermitTransferHash(the Base-USDC direct-transfer variant). Every other start validation is unchanged, andFundEphemeralstill verifies the reported hash on-chain against the issued blueprint before any platform spend./ramp/startand/ramp/updatestay strict.getFundedInitialSellRampWhere), so a deploy cannot remove a flow the worker still starts.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.bun typecheck,bun verify.No Slack alert when the worker auto-starts a ramp (decided).
🤖 Generated with Claude Code