Account Demolisher

Security

What the demolisher signs, what it never signs, and the threat model.

Account closure is one of the most destructive actions a user can take on Stellar. The design minimizes the trust the demolisher asks for.

What the deployment can do

The deployment is a Next.js application. The server side has three API routes:

  • /api/mediator/sign co-signs a strictly-validated two-op envelope using the mediator account's secret key.
  • /api/positions is a server-side proxy for the Orion and OctoPos DeFi position APIs. It conceals the upstream Bearer tokens.
  • /api/soroswap is a server-side proxy for the Soroswap aggregator. It also conceals the upstream Bearer token.

The deployment does not:

  • Hold the user's secret key. The key never reaches the server.
  • Construct transactions on the user's behalf without the user reviewing them first.
  • Submit transactions on the user's behalf. The user's wallet signs and the user's browser submits.
  • Run analytics, telemetry, or any third-party tracking.

The mediator key

The only privileged secret on the server is the mediator account's seed (MEDIATOR_SECRET). It exists so the server can co-sign the second envelope in the exchange-merge path (see Destinations and the mediator).

Limits the mediator key faces:

  • The validator at src/lib/mediator/validator.ts enforces 16 distinct shape constraints split across two endpoints. The forward envelope (mediator sends a native payment to the user destination, then merges itself) is the canonical exchange path. The merge-helper envelope (an accountMerge to the mediator plus a mediator-sourced payment or createAccount) is also accepted. Everything else, including fee-bump envelopes, is rejected.
  • The maximum time bound on the envelope is one hour. A signed envelope that doesn't get submitted within an hour is invalid.
  • The endpoint is rate limited at 5 requests per minute per IP.
  • The endpoint refuses FEE_BUMP envelopes outright.

If the validator passes, the worst the mediator key can do is what the envelope already authorizes: send the user's funds to the address the user already specified.

Wallet signing

Every transaction is signed on the user's own device. The demolisher uses Stellar Wallets Kit to talk to Freighter, xBull, Albedo, Rabet, Lobstr, Hana, and WalletConnect. The kit passes the unsigned envelope to the wallet; the wallet shows its own confirmation prompt; the user approves; the wallet returns the signed envelope. The demolisher never sees the secret key.

For users without a wallet installed, the Advanced secret-key fallback exists. The seed stays in browser memory only. It is never persisted to disk, never sent to any server, and never copied to the clipboard. A fresh Keypair is constructed for each signing call so the keypair bytes live for one method invocation only. The warning above the fallback form is real: this path is less safe than using a wallet, and is provided only because some users genuinely have no wallet.

Contract allow-list

Every Soroban transaction the demolisher signs is verified against a network-specific allow-list of contract ids before signing. The allow-list lives in src/lib/config/contracts.ts and contains 25 mainnet entries and 19 testnet entries covering Soroswap, Blend, Aquarius, and FxDAO.

A position-API response cannot extend the allow-list at runtime. Only edits to the source file change it.

If a transaction the demolisher is about to sign invokes a contract not on the allow-list, the signing call refuses and the closure halts with an AllowlistViolation error. This is a deliberate hard stop. If a position the audit found needs an unknown contract to exit, the safe move is to escalate, not to sign blindly.

Safety gates in the UI

Before any signing happens the user passes through:

  • Typed confirmation. The user must type the last four characters of the destination address, after a 5-second timer has elapsed. This catches wrong pastes and clipboard hijacks.
  • High value warning. A separate modal shown when the balance about to be moved exceeds 1000 XLM. It is not bypassable without a click on a different button.
  • Scam token heuristics. Token symbols that exactly match a tier-1 asset but come from a non-canonical issuer are flagged as critical. Symbols within edit distance 2 of a tier-1 are flagged as lookalikes. Symbols with characters outside [A-Z0-9] (homoglyphs, accented or non-Latin code points) are flagged as critical.
  • Memo enforcement. For known centralized exchanges, the configure step refuses to start without the right memo type and content.

What the threat model covers

ThreatMitigation
Clipboard or paste hijack of the destinationTyped last-4 gate plus 5-second timer
High-value blind sendHigh-value warning above 1000 XLM
Phishing clone of the deployment URLCanonical URL declared in public/stellar.toml (SEP-1) so embedders can verify the origin
Supply-chain attack on an SDK or walletpnpm-lock.yaml committed; no analytics or telemetry SDKs in the dependency graph
Position API returning malicious contractsHard-coded allow-list verified at signing time
Mediator key leakageServer-only loader, memoized; validator rejects every shape except the canonical two-op envelope; one-hour max time bound; rate limited
Front-running or MEV on a swapSlippage minimum re-applied client-side; slippage-exceeded failures classified as user-consent gates, not auto-retried
Cross-network replay of a partially-signed XDRStellar signatures bind the network passphrase into the tx hash; the partial-XDR merger admits only signatures that verify against the canonical hash

Content Security Policy

The production deployment serves a strict CSP. connect-src allows only the 12 explicitly enumerated upstream endpoints (Horizon, Soroban RPC, Refractor, the Aquarius API, Soroswap, and Friendbot). script-src allows 'self', 'unsafe-inline' (required by Next.js 16 React Server Components inline payload), and 'wasm-unsafe-eval' (required by the Stellar SDK's ed25519 WASM module). Frames are denied. Object sources are denied. Form actions are restricted to 'self'.

If a network request the deployment isn't supposed to make would happen, the browser blocks it at the CSP layer.