Create spec and security audit#1149
Conversation
User-wallet phases (moneriumOnrampMint, SELL squidRouterApprove/Swap,
squidRouterNoPermit{Approve,Swap,Transfer}) previously fell through
validatePresignedTxs via 'continue', which allowed a malicious client to
attach an unrelated presigned tx labeled with one of these phase names
without any content validation. Flip the skip to a BAD_REQUEST reject and
direct integrators to submit only the on-chain tx hash via additionalData.
Add verifyUserSubmittedTxByHash helper that resolves the receipt and
transaction by hash, then binds receipt.from, tx.to, tx.input, tx.value to
the server-issued unsigned payload (blueprint.signer + blueprint.txData).
Refactor squidrouter-permit-execution-handler.waitForUserHash to delegate
to the helper, and add verifyUserSubmittedSquidHashes at the top of
FundEphemeralPhaseHandler.executePhase so SELL standard EVM offramps
verify squidRouterApprove + squidRouterSwap on-chain before any ephemeral
funding occurs. This closes the F-041 gap where SELL squid hashes were
neither validated as presigned txs nor verified at runtime.
Update validation.test.ts: replace 3 skip-tests with 5 reject-tests
covering each user-wallet phase, plus a positive test confirming BUY
squidRouterSwap still validates as ephemeral-signed. All 50 validation
tests pass.
Update docs/security-spec/03-ramp-engine/transaction-validation.md to
document the two-layer model (reject + by-hash verification), mark F-041
as MITIGATED, and add a threat row for user-wallet phase presigned-tx
smuggling.
The literal-string override widened verifyingContract from EvmAddress
(`0x${string}`) back to plain string, breaking TypedDataDomain
assignability. Narrow the literal to the branded hex type, which is the
canonical pattern for hex-string types in viem/ethers (already used a few
lines below for sig.r / sig.s).
Server-issued unsigned txs with maxPriorityFeePerGas:'0' (or other zero minimums) were rejected when the signer produced a legacy/type-0 tx with only gasPrice, blocking BRL->USDT onramp updateRamp. A zero minimum means 'no constraint', so a missing field is acceptable; only reject if a concrete value is strictly below the minimum. Non-zero minimums still require the field to be present and meet the bound.
✅ Deploy Preview for vortex-sandbox ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for vortexfi ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
There was a problem hiding this comment.
Pull request overview
This PR adds a security specification + audit artifacts for Vortex and applies multiple security/reliability hardening changes across the API, SDK, shared package, and frontend (auth enforcement, timeouts, cleanup phases, and stronger transaction validation).
Changes:
- Add security spec documentation (auth + integrations) and OpenAPI export/type-generation tooling.
- Harden API behavior: enforce auth/ownership on ramp endpoints, introduce fetch timeouts, tighten error handling, and add additional cleanup/validation paths.
- Update transaction/ramp mechanics across chains (new cleanup phases, retry behavior, validation for user-submitted tx hashes, and phase naming normalization).
Reviewed changes
Copilot reviewed 193 out of 211 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| relayer-contract/SECURITY_AUDIT.md | Add relayer security audit report |
| packages/shared/src/services/squidrouter/route.ts | Reinstate slippage guardrail |
| packages/shared/src/services/pendulum/apiManager.ts | Cache API instances per network/index |
| packages/shared/src/services/brla/mappings.ts | Fix BRLA type import path |
| packages/shared/src/endpoints/ramp.endpoints.ts | Normalize phases; expand cleanup phases |
| packages/shared/src/endpoints/brla.endpoints.ts | Fix BRLA endpoints type imports |
| packages/shared/src/constants.ts | Centralize sandbox flag via helper |
| packages/sdk/src/services/ApiService.ts | Remove debug logging |
| packages/sdk/README.md | Update SDK usage docs + APIs |
| package.json | Add docs OpenAPI scripts |
| docs/security-spec/05-integrations/monerium.md | Add Monerium security spec |
| docs/security-spec/05-integrations/alfredpay.md | Add Alfredpay security spec |
| docs/security-spec/05-integrations/_template.md | Add integration spec template |
| docs/security-spec/01-auth/supabase-otp.md | Add Supabase OTP security spec |
| docs/security-spec/01-auth/admin-auth.md | Add admin auth security spec |
| docs/api/scripts/generate-openapi-types.ts | Generate TS types from OpenAPI |
| docs/api/scripts/export-openapi.ts | Export OpenAPI from Apidog |
| docs/api/pages/11-production-checklist.md | Add production checklist |
| docs/api/pages/10-sandbox.md | Add sandbox guide + mock accounts |
| docs/api/pages/09-brl-kyc-notes.md | Add BRL KYC integration notes |
| docs/api/pages/06-quotes-and-pricing.md | Add quotes/pricing guide |
| docs/api/pages/05-ephemeral-key-custody.md | Add ephemeral key custody guide |
| docs/api/pages/04-ramp-lifecycle.md | Add ramp lifecycle guide |
| docs/api/pages/03-authentication-and-partner-keys.md | Add auth + partner key guide |
| docs/api/pages/01-overview.md | Add API docs overview |
| docs/api/apidog/page-manifest.json | Add docs page manifest for Apidog |
| contracts/relayer/x-ray/entry-points.md | Add relayer entry-point map |
| contracts/relayer/typechain-types/index.ts | Typechain formatting update |
| contracts/relayer/typechain-types/factories/contracts/TokenRelayer__factory.ts | Typechain formatting update |
| contracts/relayer/typechain-types/@openzeppelin/contracts/utils/index.ts | Typechain formatting update |
| contracts/relayer/typechain-types/@openzeppelin/contracts/token/ERC20/index.ts | Typechain formatting update |
| contracts/relayer/typechain-types/@openzeppelin/contracts/index.ts | Typechain formatting update |
| apps/frontend/tsconfig.json | Switch TS moduleResolution to bundler |
| apps/frontend/src/services/transactions/userSigning.ts | Include optional gas override when provided |
| apps/frontend/src/pages/progress/phaseMessages.ts | Remove deprecated *Evm phase labels |
| apps/frontend/src/pages/progress/phaseFlows.test.ts | Add tests for phase flow lists |
| apps/frontend/src/main.tsx | Make Sentry DSN env-configurable |
| apps/frontend/src/machines/ramp.context.ts | Introduce initial ramp context helpers |
| apps/frontend/src/machines/ramp.actors.ts | Add ramp machine actors (quote refresh, auth refresh) |
| apps/frontend/src/hooks/ramp/useRampSubmission.ts | Minor error message formatting |
| apps/frontend/.env.example | Add optional VITE_SENTRY_DSN |
| apps/api/src/models/rampState.model.ts | Add unique constraint index name + uniqueness |
| apps/api/src/models/quoteTicket.model.ts | Remove persisted fee column from model |
| apps/api/src/index.ts | Refactor env loading + required secret validation |
| apps/api/src/database/migrations/026-add-unique-constraint-ramp-quote-id.ts | Add unique constraint on ramp_states.quote_id |
| apps/api/src/database/migrations/025-remove-quote-ticket-fee-column.ts | Drop quote_tickets.fee column |
| apps/api/src/constants/constants.ts | Remove secrets/env from constants; add subsidy fraction constant |
| apps/api/src/config/express.ts | Reduce request body limits; tighten CORS env gating |
| apps/api/src/config/database.ts | Add production SSL dialectOptions |
| apps/api/src/config/crypto.ts | Read webhook private key from config |
| apps/api/src/api/workers/cleanup.worker.ts | Expand cleanup eligibility + null-safe cleanupCompleted |
| apps/api/src/api/services/webhook/webhook-delivery.service.ts | Use fetchWithTimeout for webhooks |
| apps/api/src/api/services/transak/transak.service.ts | Use fetchWithTimeout for provider calls |
| apps/api/src/api/services/transactions/stellar/offrampTransaction.ts | Switch secrets + sandbox flag to config; adjust constants |
| apps/api/src/api/services/transactions/polygon/cleanup.ts | Add Polygon cleanup approval builder |
| apps/api/src/api/services/transactions/onramp/routes/monerium-to-evm.ts | Add Polygon cleanup tx; use shared funding account |
| apps/api/src/api/services/transactions/onramp/routes/monerium-to-assethub.ts | Add Polygon + Hydration cleanup txs; use config sandbox flag |
| apps/api/src/api/services/transactions/onramp/routes/avenia-to-evm.ts | Use shared funding account helper |
| apps/api/src/api/services/transactions/onramp/routes/avenia-to-evm-base.ts | Add Base cleanup approvals + bounded backup approval |
| apps/api/src/api/services/transactions/onramp/routes/avenia-to-assethub.ts | Add Hydration cleanup tx |
| apps/api/src/api/services/transactions/onramp/routes/alfredpay-to-evm.ts | Add Polygon cleanup tx; use shared funding account helper |
| apps/api/src/api/services/transactions/onramp/common/transactions.ts | Normalize Nabla phase names |
| apps/api/src/api/services/transactions/onramp/common/monerium.ts | Use config sandbox flag |
| apps/api/src/api/services/transactions/offramp/routes/evm-to-brl-base.ts | Add Base cleanup approvals; store evmEphemeralAddress |
| apps/api/src/api/services/transactions/offramp/routes/evm-to-alfredpay.ts | Use config secrets; add Polygon AXLUSDC cleanup approval |
| apps/api/src/api/services/transactions/offramp/routes/assethub-to-brl.ts | Split BRL offramp metadata validation |
| apps/api/src/api/services/transactions/offramp/common/validation.ts | Add quote-metadata validation helper |
| apps/api/src/api/services/transactions/offramp/common/transactions.ts | Use config sandbox flag for source network selection |
| apps/api/src/api/services/transactions/moonbeam/cleanup.ts | Use shared funding account helper |
| apps/api/src/api/services/transactions/moonbeam/balance.ts | Use shared funding account helper |
| apps/api/src/api/services/transactions/hydration/cleanup.ts | Add Hydration cleanup extrinsic builder |
| apps/api/src/api/services/transactions/common/feeDistribution.ts | Add fallback payout address + phase name normalization |
| apps/api/src/api/services/transactions/base/cleanup.ts | Add Base cleanup approval builder |
| apps/api/src/api/services/stellar/loadAccount.ts | Use shared HORIZON_URL constant |
| apps/api/src/api/services/stellar/checkBalance.ts | Use shared HORIZON_URL constant |
| apps/api/src/api/services/stellar.service.ts | Use config sandbox flag; import HORIZON_URL from shared |
| apps/api/src/api/services/slack.service.ts | Move Slack config to vars + add fetch timeout |
| apps/api/src/api/services/sep10/sep10.service.ts | Use config secrets + sandbox flag |
| apps/api/src/api/services/ramp/ramp-transaction-preparation.ts | Add preparation-kind selector helper |
| apps/api/src/api/services/ramp/ramp-transaction-preparation.test.ts | Add tests for preparation-kind selection |
| apps/api/src/api/services/ramp/helpers.ts | Use fetchWithTimeout; use config sandbox flag |
| apps/api/src/api/services/ramp/base.service.ts | Allow createRampState inside DB transaction |
| apps/api/src/api/services/quote/routes/strategies/onramp-monerium-to-evm.strategy.ts | Refactor strategy definition helper |
| apps/api/src/api/services/quote/routes/strategies/onramp-monerium-to-assethub.strategy.ts | Refactor + hydration stage helper |
| apps/api/src/api/services/quote/routes/strategies/onramp-avenia-to-evm.strategy.ts | Refactor strategy definition helper |
| apps/api/src/api/services/quote/routes/strategies/onramp-avenia-to-evm.strategy-base.ts | Refactor strategy definition helper |
| apps/api/src/api/services/quote/routes/strategies/onramp-avenia-to-assethub.strategy.ts | Refactor + hydration stage helper |
| apps/api/src/api/services/quote/routes/strategies/onramp-alfredpay-to-evm.strategy.ts | Refactor strategy definition helper |
| apps/api/src/api/services/quote/routes/strategies/offramp-to-stellar.strategy.ts | Refactor strategy definition helper |
| apps/api/src/api/services/quote/routes/strategies/offramp-to-pix.strategy.ts | Refactor strategy definition helper |
| apps/api/src/api/services/quote/routes/strategies/offramp-to-pix-base.strategy.ts | Refactor strategy definition helper |
| apps/api/src/api/services/quote/routes/strategies/offramp-evm-to-alfredpay.strategy.ts | Refactor strategy definition helper |
| apps/api/src/api/services/quote/routes/route-resolver.ts | Switch resolver to singleton strategy instances |
| apps/api/src/api/services/quote/routes/route-definition.ts | Add defineRouteStrategy + hydration stage helper |
| apps/api/src/api/services/quote/index.ts | Reject misconfigured partners for EVM payout routes |
| apps/api/src/api/services/quote/engines/finalize/onramp.ts | Enforce max amount limits |
| apps/api/src/api/services/quote/engines/finalize/offramp.ts | Enforce max amount limits |
| apps/api/src/api/services/quote/engines/finalize/index.ts | Stop persisting fee column |
| apps/api/src/api/services/quote/engines/fee/index.ts | Document fee summary as single source of truth |
| apps/api/src/api/services/quote/core/quote-fees.ts | Clamp negative fee components to zero |
| apps/api/src/api/services/priceFeed.service.ts | Move provider config to vars + add fetch timeout |
| apps/api/src/api/services/phases/register-handlers.ts | Remove deprecated EVM subsidy handlers |
| apps/api/src/api/services/phases/post-process/stellar-post-process-handler.ts | Use config sandbox flag |
| apps/api/src/api/services/phases/post-process/index.ts | Register additional post-process handlers |
| apps/api/src/api/services/phases/post-process/hydration-post-process-handler.ts | Add Hydration cleanup post-process |
| apps/api/src/api/services/phases/post-process/base-chain-post-process-handler.ts | Add Base ERC-20 sweep post-process |
| apps/api/src/api/services/phases/post-process/assethub-post-process-handler.ts | Add AssetHub handler placeholder |
| apps/api/src/api/services/phases/phase-processor.ts | Allow per-phase max retries |
| apps/api/src/api/services/phases/meta-state-types.ts | Add tx-hash tracking fields |
| apps/api/src/api/services/phases/helpers/user-tx-verifier.ts | Verify user-submitted EVM tx hashes vs blueprint |
| apps/api/src/api/services/phases/helpers/stellar-payment-verifier.ts | Use shared HORIZON_URL constant |
| apps/api/src/api/services/phases/handlers/stellar-payment-handler.ts | Persist stellarPaymentTxHash + idempotency guard |
| apps/api/src/api/services/phases/handlers/squid-router-pay-phase-handler.ts | Use shared funding account helper |
| apps/api/src/api/services/phases/handlers/spacewalk-redeem-handler.ts | Remove ts-ignore; adjust nonce reading |
| apps/api/src/api/services/phases/handlers/pendulum-to-hydration-xcm-phase-handler.ts | Add idempotency guard + arrival polling |
| apps/api/src/api/services/phases/handlers/pendulum-to-assethub-phase-handler.ts | Add idempotency guard |
| apps/api/src/api/services/phases/handlers/nabla-swap-handler.ts | Add tx-hash idempotency guard; normalize phase names |
| apps/api/src/api/services/phases/handlers/nabla-approve-handler.ts | Normalize phase names |
| apps/api/src/api/services/phases/handlers/moonbeam-to-pendulum-handler.ts | Use config secret for executor key |
| apps/api/src/api/services/phases/handlers/monerium-onramp-self-transfer-handler.ts | Use config secret for executor key |
| apps/api/src/api/services/phases/handlers/initial-phase-handler.ts | Use config sandbox flag |
| apps/api/src/api/services/phases/handlers/hydration-to-assethub-xcm-phase-handler.ts | Treat nonce mismatch as recoverable |
| apps/api/src/api/services/phases/handlers/hydration-swap-handler.ts | Persist hydrationSwapHash + idempotency guard |
| apps/api/src/api/services/phases/handlers/helpers.ts | Use config sandbox flag for Stellar passphrase |
| apps/api/src/api/services/phases/handlers/fund-ephemeral-handler.ts | Verify user squid hashes before funding; normalize phase names |
| apps/api/src/api/services/phases/handlers/final-settlement-subsidy.ts | Use shared funding account; add output sanity check |
| apps/api/src/api/services/phases/handlers/distribute-fees-handler.ts | Normalize phases; add fetch timeout; move Subscan key to config |
| apps/api/src/api/services/phases/handlers/destination-transfer-handler.ts | Validate destination recipient against presigned tx |
| apps/api/src/api/services/phases/evm-funding.ts | Centralize EVM funding account derivation |
| apps/api/src/api/services/phases/base-phase-handler.ts | Add optional per-handler max retries |
| apps/api/src/api/services/pendulum/pendulum.service.ts | Move funding seed to config |
| apps/api/src/api/services/moonpay/moonpay.service.ts | Use fetchWithTimeout for provider calls |
| apps/api/src/api/services/monerium/index.ts | Move Monerium creds to config + add fetch timeout |
| apps/api/src/api/services/auth/supabase.service.ts | Use admin client for token verification |
| apps/api/src/api/services/alchemypay/alchemypay.service.ts | Use fetchWithTimeout for provider calls |
| apps/api/src/api/routes/v1/webhook.route.ts | Require API key auth for webhook mgmt |
| apps/api/src/api/routes/v1/stellar.route.ts | Require auth for Stellar create |
| apps/api/src/api/routes/v1/ramp.route.ts | Require partner-or-user auth for ramp endpoints |
| apps/api/src/api/routes/v1/quote.route.ts | Enforce partner auth when partnerId present |
| apps/api/src/api/routes/v1/maintenance.route.ts | Gate schedules endpoints with admin auth |
| apps/api/src/api/routes/v1/index.ts | Remove deprecated routes from v1 index |
| apps/api/src/api/routes/v1/brla.route.ts | Require auth for user-scoped BRLA endpoints |
| apps/api/src/api/middlewares/error.ts | Hide 5xx messages in non-dev environments |
| apps/api/src/api/middlewares/dualAuth.test.ts | Add quote ownership tests |
| apps/api/src/api/middlewares/adminAuth.ts | Use timingSafeEqual; add audit logging |
| apps/api/src/api/helpers/fetchWithTimeout.ts | Add timeout wrapper for fetch |
| apps/api/src/api/helpers/anchors.ts | Use fetchWithTimeout for TOML fetch |
| apps/api/src/api/controllers/subsidize.controller.ts | Validate subsidy amount + use config seed |
| apps/api/src/api/controllers/stellar.controller.ts | Use config secrets for Stellar funding |
| apps/api/src/api/controllers/session.controller.ts | Use configurable widget base URL |
| apps/api/src/api/controllers/ramp.controller.ts | Enforce quote/ramp ownership checks |
| apps/api/src/api/controllers/moonbeam.controller.ts | Use config secret for executor key |
| apps/api/src/api/controllers/brla.controller.ts | Normalize/consistently persist taxId |
| apps/api/src/api/controllers/admin/partnerApiKeys.controller.ts | Use config sandbox flag for key env |
| apps/api/README.md | Update endpoint/auth docs |
| apps/api/.env.example | Expand + document new env vars |
| .gitignore | Ignore env files repo-wide (keep .env.example) |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const network = this.getNetworkConfig(networkName); | ||
| const index = wsUrlIndex ?? 0; | ||
| const wsUrl = network.wsUrls[index]; | ||
| logger.current.info(`Connecting to node ${wsUrl}...`); | ||
| const newApi = await this.connectApi(networkName, index); | ||
| const instanceKey = this.generateInstanceKey(networkName, index); | ||
| const existingInstance = this.apiInstances.get(instanceKey); |
There was a problem hiding this comment.
Removed the unused wsUrl variable from populateApi in 69904d1.
| const baseFee = STELLAR_BASE_FEE; | ||
| if (!FUNDING_SECRET) { | ||
| const NUMBER_OF_PRESIGNED_TXS = 5; | ||
|
|
There was a problem hiding this comment.
Replaced the local NUMBER_OF_PRESIGNED_TXS = 5 with the shared constant imported from @vortexfi/shared in 69904d1.
| if (pendulumToHydrationXcmHash) { | ||
| logger.info( | ||
| `PendulumToHydrationXCMPhaseHandler: Transaction already submitted (${pendulumToHydrationXcmHash}), waiting for arrival` | ||
| ); | ||
| logger.info("Waiting for assets to arrive on Hydration"); | ||
| await waitUntilTrue(didInputTokenArriveOnHydration, 60000); | ||
| return this.transitionToNextPhase(state, "hydrationSwap"); |
There was a problem hiding this comment.
Replaced waitUntilTrue(didInputTokenArriveOnHydration, 60000) with waitUntilTrueWithTimeout(didInputTokenArriveOnHydration, 5000, 120000) on the already-submitted path (matching the fresh-submission path) in 69904d1. Also removed the now-unused waitUntilTrue import.
| dialect: config.database.dialect, | ||
| dialectOptions: | ||
| config.env === "production" | ||
| ? { | ||
| ssl: { | ||
| rejectUnauthorized: false, | ||
| require: true | ||
| } | ||
| } | ||
| : undefined, |
There was a problem hiding this comment.
Changed rejectUnauthorized: false to process.env.DB_SSL_REJECT_UNAUTHORIZED !== "false" in 69904d1. This defaults to true (validates certificates) and provides an explicit opt-out via the DB_SSL_REJECT_UNAUTHORIZED=false env var for environments where certificate validation can't be used.
| ### Euro Onramps | ||
| - **Login Method**: Sign in using an EVM wallet. | ||
| - **Test Wallet**: | ||
| - Public Address: `0x6f64A6a3eBB0Fa2F265bB173407cb2A90AE0D32f` | ||
| - Recovery Phrase: `sword joke bomb old couch junior dumb need story grace spirit casual` | ||
| - **Note**: This wallet is pre-loaded with testnet funds. |
There was a problem hiding this comment.
Removed the recovery phrase from the sandbox docs and replaced it with a note to use a freshly generated test wallet in 69904d1.
| export class BaseChainPostProcessHandler extends BasePostProcessHandler { | ||
| public getCleanupName(): CleanupPhase { | ||
| return "baseCleanupBrla"; | ||
| } |
There was a problem hiding this comment.
Overrode createErrorObject in BaseChainPostProcessHandler to accept an optional CleanupPhase parameter, and updated all sweepToken error sites to pass the actual phase. Errors now correctly report the phase that failed (e.g. baseCleanupUsdc) rather than always showing baseCleanupBrla — fixed in 69904d1.
…UntilTrue timeout, SSL cert validation, seed phrase, cleanup phase label Agent-Logs-Url: https://github.com/pendulum-chain/vortex/sessions/57d9c30a-ecbd-4c12-b598-95ed6c21e347 Co-authored-by: ebma <6690623+ebma@users.noreply.github.com>
In this PR, a security spec for Vortex is created that is used to perform an AI audit. The relevant audit findings where fixed. It also includes misc other improvements for security and transaction validation.