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/signco-signs a strictly-validated two-op envelope using the mediator account's secret key./api/positionsis a server-side proxy for the Orion and OctoPos DeFi position APIs. It conceals the upstream Bearer tokens./api/soroswapis 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.tsenforces 16 distinct shape constraints split across two endpoints. The forward envelope (mediator sends a nativepaymentto the user destination, then merges itself) is the canonical exchange path. The merge-helper envelope (anaccountMergeto the mediator plus a mediator-sourcedpaymentorcreateAccount) 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_BUMPenvelopes 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
| Threat | Mitigation |
|---|---|
| Clipboard or paste hijack of the destination | Typed last-4 gate plus 5-second timer |
| High-value blind send | High-value warning above 1000 XLM |
| Phishing clone of the deployment URL | Canonical URL declared in public/stellar.toml (SEP-1) so embedders can verify the origin |
| Supply-chain attack on an SDK or wallet | pnpm-lock.yaml committed; no analytics or telemetry SDKs in the dependency graph |
| Position API returning malicious contracts | Hard-coded allow-list verified at signing time |
| Mediator key leakage | Server-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 swap | Slippage minimum re-applied client-side; slippage-exceeded failures classified as user-consent gates, not auto-retried |
| Cross-network replay of a partially-signed XDR | Stellar 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.