DEV Community

Cover image for Integrating a Swap Provider? Here's the Threat Model You Inherit
Swapcore.Exchange
Swapcore.Exchange

Posted on

Integrating a Swap Provider? Here's the Threat Model You Inherit

"Non-custodial" in a swap provider's docs usually gets read as "not my security problem." That's half right. Integrating a non-custodial instant exchange removes the biggest risk class, which is holding user balances. But it hands your users a different, smaller one, and parts of that risk run straight through your frontend.

Here's the threat model, split by who owns each piece.

First, get the custody model right

A non-custodial instant exchange isn't zero-custody. The flow looks like this:

user wallet ──deposit──▶ provider deposit address ──swap──▶ user wallet
   (user keys)            (PROVIDER KEYS: transit)           (user keys)
Enter fullscreen mode Exit fullscreen mode

From deposit confirmation until payout, the provider controls the funds. The window is short, usually minutes, bounded by confirmation time on the input chain. But it exists, and any honest threat model starts from it.

What your app never holds: keys, balances, deposits. That's the real win. You're not a custodian, you don't carry a hot wallet, and your breach doesn't drain anyone.

Threats the provider owns

Transit-window compromise. If the provider's operational wallets are breached, in-flight funds are exposed. This has happened in this category: FixedFloat lost around $26M from its own wallets in February 2024. You can't mitigate it in code. You mitigate it in vendor selection (incident history, how they handled it, whether in-flight users were made whole) and by keeping per-swap exposure small.

AML holds. Deposits get screened, and flagged ones pause. That's the provider's process, but surfacing it is your job. Give it a distinct held_for_review state and don't fold it into pending.

Availability. Rate feeds stall, pairs get disabled, minimums spike with congestion. Treat every quote as perishable and every pair as possibly unavailable.

Threats you own

This is the part integrators underestimate. Your frontend is the only place several of these can be caught.

Address substitution. Clipboard malware swaps a copied address for the attacker's. Your UI can't detect malware, but it can make substitution visible:

// Render the destination with its first and last 6 chars emphasized,
// and restate it on the confirmation step. Visual diffing catches
// most substitutions, which change the whole string.
function renderAddress(addr) {
  return {
    head: addr.slice(0, 6),
    middle: addr.slice(6, -6),
    tail: addr.slice(-6),
  };
}
Enter fullscreen mode Exit fullscreen mode

Chain ambiguity. EVM addresses carry no chain identity. Never infer the chain from an address. Make it a required selection with no default, and restate it in words at confirmation.

Conditional memos. For XRP, XLM, ATOM, HBAR, EOS and TON, a memo is required when the destination is a shared (exchange) address and meaningless when it's self-custody. Ask which one it is, and make the field blocking only when it applies.

Refund routing. Refunds go to the sender address by default. If the user funded from an exchange withdrawal, that's a pooled hot wallet and the refund is effectively lost. Collect a refund address at order creation:

const order = await provider.createOrder({
  pair,
  amount,
  destination,
  refundAddress: connectedWallet ?? userInput.refundAddress, // never null silently
});
Enter fullscreen mode Exit fullscreen mode

Clone-site exposure. If you embed a widget, load it from the provider's canonical origin and pin it in your CSP. A compromised or typo-squatted widget source is a deposit-address swap at scale.

Content-Security-Policy: frame-src https://widget.<provider-domain>;
Enter fullscreen mode Exit fullscreen mode

Quote integrity. If quotes pass through your backend, sign or verify them end-to-end. A deposit address that can be altered anywhere between provider and user is the single highest-value target in your stack.

Threats nobody in the stack owns

Worth stating in your docs so users aren't surprised:

  • Key loss: self-custody moves it entirely to the user
  • Malicious token approvals signed elsewhere, which live in the user's wallet
  • Issuer freezes: Tether can freeze USDT at the contract level on any chain

Shrinking the transit window by design

The one risk the model adds is time in transit. Design to minimise it:

  • Prefer fast input chains in your defaults when the user holds the asset on several
  • Don't create the order until the user is ready to send. Generate the deposit address at the last step, not the first, so abandoned sessions don't leave stale addresses around.
  • Offer split execution above a threshold. Several moderate swaps cap peak exposure below one large one.

Summary table

Threat Owner Mitigation lives in
Transit-window breach Provider Vendor selection, size limits
AML hold Provider Your state machine
Address substitution You Confirmation UI
Wrong chain You Required selection, restatement
Missing memo You Conditional blocking field
Lost refunds You Refund address at creation
Widget/clone tampering You Canonical origin, CSP
Key loss, bad approvals, issuer freeze User Documentation

Non-custodial removes the catastrophic risk: holding everyone's money. What's left is smaller, but most of it gets caught or missed in your frontend.


Disclosure: I work on SwapCore (swapcore.exchange), a non-custodial instant exchange. We have a transit window like every provider in this category, and the mitigations above are the ones I'd expect any integrator to ask us about.

Top comments (0)