> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lumenwipe.com/llms.txt
> Use this file to discover all available pages before exploring further.

# STRIDE threat model

> A structured, per-surface STRIDE analysis of key handling, the API's two signing keys, transaction construction, and the client-side session layer.

> This document formalizes what [Section 13](/architecture#13-security-model) of the technical architecture
> already narrates in prose. It is not a rewrite of that section - it restates the same design as a
> structured STRIDE analysis (Spoofing, Tampering, Repudiation, Information disclosure, Denial of service,
> Elevation of privilege) so a reader, an auditor, or the security-tooling remediation plan can check specific
> threats against specific mitigations, one surface at a time.

## 1. Scope and method

In scope, one STRIDE pass per surface:

1. Client-side key handling (wallet path and the secret-key advanced mode).
2. The client-side session layer (the `DemolishPhase` state machine and IndexedDB session store).
3. API transaction construction (the pure transaction-builder module).
4. The two backend signing keys: the mediator co-sign key, and the fee-bump sponsor key.
5. Allowance revocation (Tranche 2, epic #159).
6. Soroban token conversion via Soroswap (Tranche 2, epic #159).
7. DeFi exit adapters - Blend, Aquarius, Soroswap router (Tranche 2, epic #151).

Out of scope, deliberately:

* **Read-only external data sources** (RPC providers, the Horizon-compatible enumeration endpoint, the
  Soroswap routing API, OctoPos). These carry availability risk, not custody risk - they cannot move funds -
  and are already addressed as a trust-minimization concern in [Section 14](/architecture#14-trust-minimization-and-decentralization),
  not repeated here.
* **A third-party penetration test or formal audit.** Tracked separately, deferred by design (epic #166),
  outside this document's scope.
* **Infrastructure-level threats** (cloud provider compromise, CI/CD supply-chain attacks against the deploy
  pipeline). Covered by [Section 15](/architecture#15-infrastructure-and-deployment)'s deployment model, not
  re-analyzed here.

Every mitigation cited below is either quoted from `docs/architecture.md` or traced to the specific source
file that implements it - this document verifies existing controls, it does not propose new ones.

## 2. Surface 1: client-side key handling

Two paths: the wallet path (`stellar-wallets-kit`, primary) and the secret-key advanced mode
(`apps/web/lib/stellar/signer.ts`), for keys not held in any wallet.

| Threat category        | Threat                                                                                               | Mitigation                                                                                                                                                                                                                                                                                      |
| ---------------------- | ---------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Spoofing               | A malicious page or script impersonates the signing UI to harvest a key                              | The secret-key input is a password field rendered only within the signing step's own component tree; a strict CSP (`docs/architecture.md` §13.1) blocks inline scripts and external script sources that could inject a fake input                                                               |
| Tampering              | A compromised dependency reads the key out of memory                                                 | Lockfile-pinned dependencies, audited in CI; no dependency permitted that needs dynamic code execution (§13.1)                                                                                                                                                                                  |
| Repudiation            | N/A - key handling does not produce an audit trail claim                                             | Not applicable to this surface                                                                                                                                                                                                                                                                  |
| Information disclosure | The key persists somewhere it can later be read (storage, logs, network)                             | `SecretKeySigner` (`apps/web/lib/stellar/signer.ts`) holds the parsed `Keypair` only as a private instance field; nothing in the signing path writes to `localStorage`, `sessionStorage`, IndexedDB, cookies, or any outbound request (§13.2)                                                   |
| Denial of service      | N/A - unavailability of this surface blocks only the current user's own close, not a shared resource | Not applicable at this trust boundary                                                                                                                                                                                                                                                           |
| Elevation of privilege | A held key outlives the session and gets reused for an action the user didn't confirm                | Wiped on completion, on abort, on navigation away from the flow, and on explicit "Forget key"; for multisig, keys are gathered one at a time and only cleared by switching signer or forgetting, not per signature (§13.2). The component holding the key unmounts on leaving the signing step. |

Wallet-path signing carries a narrower version of the same threats: the key never enters the application at
all, so Information disclosure and Tampering against in-memory key material do not apply - the residual
threats are wallet-extension compromise and CSP bypass, both outside this surface's boundary.

## 3. Surface 2: the client-side session layer

`apps/web/store/demolish.ts` (the `DemolishPhase` state machine) and `apps/web/lib/session/store.ts` (the
IndexedDB-backed session store, via `idb`), plus `verify()` (`apps/web/lib/stellar/verify.ts`) as the trust
anchor gating every signature this layer eventually authorizes.

| Threat category        | Threat                                                                                       | Mitigation                                                                                                                                                                                                                                                                               |
| ---------------------- | -------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Spoofing               | A crafted API response gets signed as if it matched the user's own request                   | `verify()` sources its expected destination, memo, asset, and amount from the user's own inputs, never from the API response (§13.1); a mismatch aborts before signing                                                                                                                   |
| Tampering              | A resumed session executes a step the user never reviewed                                    | The `PREFLIGHT_COMPLETE → STEP_EXECUTING` transition only fires from the `/review` page's own explicit confirmation; nothing is written to the resumable session store before that fires, so a tab closed mid-review has nothing to resume (§13.3)                                       |
| Repudiation            | The user disputes having authorized a step that in fact ran                                  | Every destructive step requires an explicit acknowledgment naming the affected entry/balance before submission; the tool never auto-submits (§13.3)                                                                                                                                      |
| Information disclosure | The persisted session record leaks key material or other sensitive data from IndexedDB       | Verified directly against `apps/web/lib/session/store.ts`: the `SessionRecord` schema persisted via `saveSession`/`loadSession` carries plan and progress state, not signing material - consistent with §13.2's claim that the key never touches any storage layer                       |
| Denial of service      | A malformed or corrupted session record blocks the user from resuming or restarting          | The API is stateless per round and re-reads live account state every call (`remaining.requiresAnotherCall`); an interrupted close resumes by calling again rather than reconciling stored server-side progress, so a corrupted local session degrades to "start over," not a stuck state |
| Elevation of privilege | An unrelated operation gets appended to a transaction and signed alongside the reviewed plan | `verify()`'s allowlist rejects any operation shape it does not recognize (`docs/architecture.md`, "Consequence for any new close operation"); an unknown operation aborts signing rather than being silently accepted                                                                    |

## 4. Surface 3: API transaction construction

The API's transaction-builder module (`apps/api`) is a pure module by design invariant - state in, unsigned
envelopes out, no network side effects (CLAUDE.md, "Hard invariants") - which is what makes it possible for
`verify()` to treat its output as untrusted input rather than a peer.

| Threat category        | Threat                                                                                                                     | Mitigation                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Spoofing               | A compromised or buggy API builds a transaction that diverts funds to an unintended destination                            | `verify()` re-derives every expected value from the user's own choices and never signs bytes it did not itself verify (§13.1); a compromised API cannot get funds diverted through the builder alone                                                                                                                                                                                                                                           |
| Tampering              | The builder is fed stale account state and constructs a transaction against on-chain reality that has since changed        | The API re-reads exact on-chain state over RPC immediately before building (CLAUDE.md, "Hard invariants" - "Never build or sign from indexer data alone")                                                                                                                                                                                                                                                                                      |
| Repudiation            | N/A - the builder does not itself authorize anything; authorization happens at signing                                     | Not applicable to this surface                                                                                                                                                                                                                                                                                                                                                                                                                 |
| Information disclosure | Builder errors leak internal state (stack traces, SDK error codes) to the client                                           | User-facing errors are plain language; raw SDK codes or stack traces never surface (CLAUDE.md, "Hard invariants")                                                                                                                                                                                                                                                                                                                              |
| Denial of service      | A position that cannot be safely closed causes the builder to fail unsafely (partial execution, silent skip)               | A position or step that cannot be closed safely surfaces as an explained blocker, never silently skipped (CLAUDE.md, "Hard invariants")                                                                                                                                                                                                                                                                                                        |
| Elevation of privilege | A `SetOptions` operation the builder emits adds a signer or raises thresholds, silently expanding control over the account | `verify()`'s allowlist specifically asserts `SetOptions` never adds a signer or raises thresholds (§"The trust boundary moved to verify()"); the one check sourced from the API's own trust domain rather than the user - that a signer removal targets a signer that actually exists - is cross-checked against a separate account-state read, not the transaction itself, so it also catches a builder bug independent of `verify()` (§13.1) |

## 5. Surface 4a: backend signing key — mediator co-sign

`apps/api/src/mediator/mediator.controller.ts` and `mediator-validation.ts`. The mediator is the one signing
key the API holds today; it co-signs only the forwarding payment of the exchange-mediator flow
([Section 11](/architecture#11-the-mediator-account-flow-for-exchanges)).

| Threat category        | Threat                                                                                                                                    | Mitigation                                                                                                                                                                                                                                                                                           |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Spoofing               | A caller submits a transaction shaped to make the mediator sign something other than its own forward payment                              | `MediatorController.sign` requires exactly two operations - `accountMerge` into the mediator, `payment` sourced from the mediator - and rejects any other shape (`transaction_structure_not_allowed`)                                                                                                |
| Tampering              | The forward payment's destination or amount is altered after the user set them                                                            | The mediator signs the exact transaction it validated; changing either after co-signing invalidates the merge signature the user already applied, since both operations share one envelope                                                                                                           |
| Repudiation            | The mediator denies having co-signed a forward it did in fact sign                                                                        | The mediator's signature is a standard ed25519 signature over the envelope, verifiable on-chain like any other signer - out of scope to add further non-repudiation controls here                                                                                                                    |
| Information disclosure | The mediator's secret key leaks through logging or an error path                                                                          | The mediator secret lives only in the API's environment (`MEDIATOR_SECRET_*`), never in the browser, and is used exclusively inside `getMediatorKeypair` (§13.2)                                                                                                                                     |
| Denial of service      | The mediator is spammed with malformed transactions to exhaust request budget                                                             | Per-key rate limiting at the service layer (`@ApiResponse 429`); malformed requests fail fast on `Transaction` XDR parse before any account-state read                                                                                                                                               |
| Elevation of privilege | The mediator is tricked into paying its own fee or consuming its own sequence number, or forwarding more than the merge actually delivers | `sign` explicitly rejects the mediator as `tx.source` or as the merge's source (`transaction_structure_not_allowed`); `forwardExceedsMergedBalance` bounds the forward to the merged account's native balance minus fee, checked against a balance read at co-sign time, fail-closed on read failure |

### Known residual risk: mediator forward TOCTOU

`forwardExceedsMergedBalance` (`apps/api/src/mediator/mediator-validation.ts`) documents, in its own source
comment, a genuine time-of-check/time-of-use gap: the bound is checked against a balance read taken at
co-sign time, but the merge's actual delivery is decided at submit time, which the caller controls. An
adversary who controls the merged account can drain it after co-signing (draining does not consume the
co-signed transaction's sequence number, since it is a separate operation from a separate account), then
submit - so `op0` (the merge) delivers close to nothing while `op1` (the forward) still pays out the amount
that was valid at co-sign time, sourced from the mediator's own balance rather than the merge.

This check is explicitly defense-in-depth, not a complete guarantee, against that active-adversary case. The
primary control is operational, not code-level: **the shared mediator is funded to its base reserve only and
holds no spendable surplus**, so even a successful exploitation of this gap has nothing to forward beyond
dust. The check in code stops the passive cases - a client bug, rounding dust, or a naive over-forward -
and raises the bar for the active case. This residual risk is carried forward into the summary table in
Section 10 as an accepted risk with an operational (not code) compensating control, which is the shape
`#171`'s remediation plan expects findings to already be in.

## 6. Surface 4b: backend signing key — fee-bump sponsor

**Status: implemented (#164, epic #159).** This surface is specified in
[Section 8.1](/architecture#81-sponsored-fees-closing-accounts-that-cannot-pay-their-own-way) of the
architecture doc and built in `apps/api/src/fee-bump/` (`fee-bump.controller.ts`,
`fee-bump-validation.ts`) and `apps/api/src/lib/stellar/fee-account.ts`. Every row below has been
re-verified against the running code, with test coverage listed in Section 11; two mitigations this
table originally described - the account/IP dimension of rate limiting and the fee account's
spend cap with alerting - turned out to be operational controls this PR does not implement in
code, and are carried instead as residual risks in Section 10, the same way the mediator's own
funding discipline is.

| Threat category        | Threat                                                                                                              | Mitigation                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Spoofing               | An unrelated transaction gets the fee account's sponsorship                                                         | The API sponsors an inner transaction only if every operation matches an explicit wind-down allowlist (`ChangeTrust` limit 0, `ManageSellOffer`/`ManageBuyOffer` amount 0, `ManageData` removals, `SetOptions` signer normalization, `ClaimClaimableBalance`, `PathPaymentStrictSend`, `AccountMerge`) AND every operation acts for the same account (`isAllowedWindDownOperation`, `actsForOneAccount`) - a caller cannot bundle wind-down-shaped operations for several unrelated accounts into one sponsored envelope. Neither check pins `AccountMerge`'s or `PathPaymentStrictSend`'s destination/asset/amount, a residual risk in Section 10 |
| Tampering              | The API alters an operation, amount, or destination while wrapping the fee-bump envelope                            | The inner transaction's own signature covers its contents; the API signs only the outer fee-bump envelope, and never rebuilds or edits the inner transaction it was handed, so altering it would invalidate the user's signature before the fee account ever pays                                                                                                                                                                                                                                                                                                                                                                                  |
| Repudiation            | N/A - same reasoning as the mediator key: the outer signature is independently verifiable                           | Not applicable beyond standard signature verification                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| Information disclosure | The fee-account secret leaks the same way any server-side secret could                                              | Same isolation pattern as the mediator secret: environment-only (`FEE_ACCOUNT_SECRET_{MAINNET,TESTNET}`), never transmitted to the browser - unlike the mediator, the browser never even needs to recognize this account's address, since it is never a party the client's own transaction names                                                                                                                                                                                                                                                                                                                                                   |
| Denial of service      | The fee account's operational float is drained by repeated sponsorship requests, denying the feature to other users | The outer fee is capped per transaction (`MAX_FEE_BUMP_STROOPS`, checked against the built envelope's own fee); every route including this one is rate-limited per API key by the existing global throttler. Per-Stellar-account and per-IP limiting, and the fee account's own daily spend cap with alerting, are not implemented here - see Section 10                                                                                                                                                                                                                                                                                           |
| Elevation of privilege | A sponsored transaction is replayed to drain the fee account a second time                                          | Structurally impossible: the inner transaction consumes the source account's own sequence number, so a resubmission after the first successful submission fails at the network level, not at the API's validation. The fee-bump wrapper itself carries no sequence number of its own to replay                                                                                                                                                                                                                                                                                                                                                     |

## 7. Surface 5: allowance revocation

**Status: implemented (#162/#163, epic #159).** Entirely independent of the account-close flow -
`apps/web/lib/stellar/verify-revoke-allowance.ts` is its own standalone trust anchor, not a branch
of `assertCloseIntent`, gating the one-operation `approve(owner, spender, 0, expiration_ledger)`
transaction that `apps/api/src/lib/stellar/revoke-allowance.ts` builds to zero a live SEP-41
allowance discovered by `apps/api/src/lib/stellar/allowances.ts`.

| Threat category        | Threat                                                                                                       | Mitigation                                                                                                                                                                                                                                                                                                    |
| ---------------------- | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Spoofing               | The API returns a transaction that revokes a different token or spender than what the inspector table showed | `verifyRevokeAllowanceTransaction` re-checks the built XDR against exactly `{owner, token, spender}` as displayed: `op.contract === expected.token`, `args[1] === expected.spender`, `args[2] === "0"` literal - a mismatch on any of these aborts before signing                                             |
| Tampering              | The transaction is altered after being shown, e.g. a different amount or a nonzero one                       | Same check as above; `args[2] === "0"` is asserted literally, not merely "less than before"                                                                                                                                                                                                                   |
| Repudiation            | N/A - a standard signature, independently verifiable like any other                                          | Not applicable to this surface                                                                                                                                                                                                                                                                                |
| Information disclosure | N/A - no new secret or credential this surface introduces                                                    | Not applicable to this surface                                                                                                                                                                                                                                                                                |
| Denial of service      | A revocation that cannot be built or simulated safely is offered for signing anyway                          | `RevokeAllowanceBlockedError` turns an unsafe build into its own typed error response rather than ever handing back a transaction (`revoke-allowance.ts`)                                                                                                                                                     |
| Elevation of privilege | A hidden sub-invocation authorizes something beyond the plain revocation itself                              | `op.authDepth !== 0` refuses **any** nested call outright - stricter than the close flow's DeFi-exit branch (Section 9), so the sub-invocation-hiding gap issue #208 fixed there cannot occur on this surface at all: revocation has no legitimate reason to authorize anything beyond the one `approve` call |

Even a wrong or hostile `{token, spender}` pair from the discovery endpoint (an out-of-scope, read-only
source per Section 1) bounds its own damage structurally: `approve(owner, spender, 0, _)` can only ever
zero out one specific allowance - it has no path to move the account's actual balance, unlike a
diverted destination or amount would.

## 8. Surface 6: Soroban token conversion via Soroswap

**Status: implemented (#161, epic #159).** `apps/web/lib/stellar/verify.ts`'s `invoke_host_function`
branch for a contract in `expected.conversionContracts` (the bundled registry's Soroswap aggregator
and router, never the API), fed by `apps/api/src/lib/soroswap/conversion-quotes.ts` on the build side.

| Threat category        | Threat                                                                                     | Mitigation                                                                                                                                                                   |
| ---------------------- | ------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Spoofing               | A swap exchanges a token the user never chose to convert                                   | `expected.tokenConversions[swap.token]` must exist for the exact token spent, sourced from the user's own decision, never the transaction                                    |
| Tampering              | A swap pays its proceeds to an address other than the account, or delivers less than shown | `swap.destination !== expected.source` and `swap.minAmountOut < chosen.minAmountOut` both abort; the floor is the user's own accepted quote, not the plan under verification |
| Repudiation            | N/A                                                                                        | Not applicable to this surface                                                                                                                                               |
| Information disclosure | N/A                                                                                        | Not applicable to this surface                                                                                                                                               |
| Denial of service      | N/A - unavailability here blocks only the current user's own conversion                    | Not applicable at this trust boundary                                                                                                                                        |
| Elevation of privilege | The signature would authorize more than the swap call itself                               | `op.authorizesBeyondSelf` and `op.unsupportedAddressCount` both gate the same way as every other branch; every `accountsReferenced` entry must be the account being closed   |

**Documented residual, not a gap:** unlike the DeFi-exit branch (Section 9), this branch's own source
comment states plainly that the authorization tree's sub-invocations - the actual DEX route the
aggregator picks - are **not** pinned client-side at all: *"the browser has no way to enumerate them
\[the pools a route passes through]... the API pins them."* This is a deliberate, already-documented
trust boundary (`docs/architecture.md` §10.1), not an oversight comparable to #208 - the floor above
(`minAmountOut`) is what bounds the damage a route the client cannot enumerate could otherwise do.
Carried forward as a residual risk in Section 10.

## 9. Surface 7: DeFi exit adapters (Blend, Aquarius, Soroswap router)

**Status: implemented (epic #151).** `apps/web/lib/stellar/verify.ts`'s DeFi-exit branch, fed by
`apps/web/lib/stellar/exit-expectations.ts` (which builds `exitContracts`/`heldTokenContracts`/
`positionTokenContracts`/`exitFunctions` from the account read the user reviewed, never from the
transaction under verification), backed on the API side by the versioned wasmHash contract registry
(`apps/api/src/lib/contract-registry/index.ts`) and the exit adapters themselves
(`apps/api/src/lib/defi-exits/{blend,aquarius,soroswap}.ts`).

| Threat category        | Threat                                                                                                                                                                                   | Mitigation                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Spoofing               | An exit invokes a contract the account has no detected position in                                                                                                                       | `exitContracts` (client) pins to what the account read showed; separately, the API's `resolveWasmHash` refuses to build against a contract whose bytecode hash is not in the registry at all - "the caller's only safe move is to flag the position for manual review and build nothing" (registry module's own docstring, architecture.md §9.9)                                                                                                                                                                |
| Tampering              | A legitimate top-level call (a pool's `submit`, a router's `remove_liquidity`) hides a diversion in a nested authorized sub-invocation on a token the account holds or has a position in | **Closed by #208.** Every sub-invocation on a held or position token is pinned to a SEP-41 function an exit actually needs (`transfer`, `transfer_from`, `approve`, `burn`, `burn_from`), with the recipient/spender required to be the account being closed or the exit contract at the root of that exact invocation - never a third party. Before #208, only the top-level call and a flat contract-address allow-list were checked, which a hostile sub-invocation on a contract-typed recipient could pass |
| Repudiation            | N/A                                                                                                                                                                                      | Not applicable to this surface                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| Information disclosure | N/A                                                                                                                                                                                      | Not applicable to this surface                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| Denial of service      | An exit is built against a contract whose interface the registry cannot confirm                                                                                                          | An unresolved wasmHash refuses to build rather than guessing at an interface, surfacing as an explained blocker (CLAUDE.md, "Hard invariants") instead of a failed or unsafe transaction                                                                                                                                                                                                                                                                                                                        |
| Elevation of privilege | A pinned contract is called with a function this project does not use to leave that protocol                                                                                             | `exitFunctions` restricts each contract to the small, fixed set of functions that legitimately exits or claims from that protocol - a Blend or Aquarius pool's `submit`/`claim` or `withdraw`/`claim`, a Blend backstop's `withdraw`, a Soroswap router's `remove_liquidity` - never an arbitrary call the contract happens to expose. The contract addresses alone are not enough                                                                                                                              |

**Documented residual, not a gap:** the exact amount an exit moves, and the protocol-level meaning of
the call beyond function/recipient shape, are not checked client-side - the browser cannot re-simulate
a DeFi protocol's internal accounting. Issue #208's own proposal explicitly scoped this out ("keep the
amount unconstrained - the pool decides it"). Carried forward as a residual risk in Section 10.

## 10. Known residual risks (summary)

| Risk                                                                                                                                                                                                                  | Surface                                  | Compensating control                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | Status                                                                                                                                   |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| Mediator forward TOCTOU: an adversary who controls the merged account can drain it between co-sign and submit, so the forward pays out against a balance that no longer arrives                                       | Mediator co-sign (Section 5)             | Operational: the mediator is funded to base reserve only and holds no spendable surplus                                                                                                                                                                                                                                                                                                                                                                                                            | Accepted - code-level check is defense-in-depth only                                                                                     |
| One `verify()` check (signer-removal target existence) is sourced from the API's own state read rather than the user's input                                                                                          | API transaction construction (Section 4) | A wholly compromised API could in principle keep that read and the transaction consistent with each other; every other check is user-sourced                                                                                                                                                                                                                                                                                                                                                       | Accepted - documented in `docs/architecture.md` §13.1 as the one exception to "expected values come from the user"                       |
| Fee-bump sponsorship is rate-limited per API key (the existing global throttler), not per Stellar source account or per client IP, and the fee account carries no coded daily spend cap or balance alerting           | Fee-bump sponsor (Section 6)             | Operational, mirroring the mediator's: fund the account to a small float and monitor its balance by hand until dedicated tooling exists; the per-transaction fee cap (`MAX_FEE_BUMP_STROOPS`) already bounds any single request                                                                                                                                                                                                                                                                    | Accepted - no code changes planned for #164; revisit if usage volume makes it a real cost                                                |
| `AccountMerge`'s and `PathPaymentStrictSend`'s destination, asset, and amount are unconstrained: a caller can get any of their own accounts merged, or a strict-send payment sent, to any address, sponsored for free | Fee-bump sponsor (Section 6)             | Structural, not code: the fee account's signature only ever authorizes the fee account's own payment of the outer envelope's fee. It cannot substitute for or forge the inner transaction's own required signature, so nothing sponsored here can move funds the caller does not already control by holding that signature - the residual is fee-account cost for operations outside a genuine reserve-locked close, which is the same cost the rate-limiting row above already bounds and accepts | Accepted - would need session-aware state this stateless endpoint does not hold; revisit if abused as a general-purpose free-fee service |
| A token conversion's authorization tree can invoke any pool or adapter along the DEX route the client cannot enumerate ahead of time - only the swap's destination and minimum output are pinned, not the route       | Soroban token conversion (Section 8)     | The route is pinned server-side by the bundled registry at build time; the client-side floor (`minAmountOut`) bounds the damage a route it cannot see could otherwise cause                                                                                                                                                                                                                                                                                                                        | Accepted - documented in `docs/architecture.md` §10.1; the browser has no way to enumerate route-dependent pool addresses in advance     |
| A DeFi exit's amount, and the protocol-level meaning of its call beyond function/recipient shape, are not checked client-side                                                                                         | DeFi exit adapters (Section 9)           | The API's own transaction-builder invariants and adapter-level checks are the only guard on amount/semantics; the client-side anchor can only vouch for shape, not protocol correctness                                                                                                                                                                                                                                                                                                            | Accepted - issue #208 itself scoped this out; would need the client to re-simulate protocol internals to close                           |

None of these risks are new in kind: the first two are already narrated in `docs/architecture.md`, and the
rest follow the same "operational discipline" or "documented, bounded trust" pattern the earlier rows
establish. Restating them here is the point of formalizing the threat model - they are now indexed by surface
and category instead of embedded in prose, and this is the table #171's tooling remediation plan cross-checks
its own findings against.

## 11. Coverage cross-reference

What already holds under automated test, as of this document, not just what is designed:

| Surface                                                                                             | Test coverage                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Client-side key handling                                                                            | `apps/web/lib/stellar/signer.ts` signer implementations (secret-key, hash(x) preimage, wallet-kit delegate)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| Client-side session layer / `verify()`                                                              | `apps/web/tests/unit/verify.test.ts`, `apps/web/tests/unit/verify-revoke-sponsorship.test.ts`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| API transaction construction                                                                        | Unit tests over the transaction builder (highest-coverage module per §17) plus the adversarial suite below                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| Mediator co-sign                                                                                    | `apps/api/tests/unit/mediator-validation.test.ts`, `apps/api/tests/unit/mediatorMerge.test.ts`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| Adversarial/hostile-state coverage (cross-cutting, exercises the builder and verification together) | `apps/api/tests/adversarial/`: sponsoring accounts, the 1000-subentry maximum, revoked trustlines, multisig with hash(x)/pre-auth signers, undercollateralized vaults, queued backstop withdrawals, high-slippage conversions, and lost-confirmation retry safety - running in CI on every change (#191)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| Fee-bump sponsor                                                                                    | `apps/api/tests/unit/fee-bump-validation.test.ts` (the wind-down allowlist and `actsForOneAccount`, including SDK parsing quirks the allowlist depends on: a removed trustline's limit renders as `"0.0000000"` not `"0"`, and several unset `SetOptions` fields parse as `null` rather than `undefined`), `apps/api/tests/unit/fee-bump-controller.test.ts` (end-to-end sponsorship, network misconfiguration including a malformed secret, non-zero inner fee, nested fee-bump, disallowed operations, operations bundled across more than one account), plus the malformed-body and not-configured contract cases in `apps/api/tests/e2e/contract.e2e.test.ts`. Detection of which transaction needs sponsoring, and the running per-chunk balance that judges a multi-transaction round: `apps/api/tests/unit/sponsored-fee.test.ts`. The client wiring added in #165: `apps/web/tests/unit/fee-bump-sponsor.test.ts` (the proxy-calling helper's error handling) and the `needsSponsoredFee` cases in `apps/web/tests/unit/use-close-execution.test.tsx` (the wrapped envelope is submitted; a sponsor response wrapping a different inner transaction, or one that isn't a fee-bump transaction, is rejected before submission) |
| Allowance revocation                                                                                | `apps/web/tests/unit/verify-revoke-allowance.test.ts` (the standalone anchor: token/spender/amount pinning, the `authDepth !== 0` refusal), `apps/api/tests/unit/revoke-allowance.test.ts` (the build side), `apps/api/tests/unit/allowances.test.ts` (discovery: event scanning, registry cross-reference, account-type spenders), `apps/web/tests/unit/revoke-allowance-modal.test.tsx` and `apps/web/tests/unit/allowance-row.test.tsx` (the UI's own build→verify→sign→submit flow)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| Soroban token conversion                                                                            | `apps/web/tests/unit/verify.test.ts` (router- and aggregator-shaped swaps, the floor and destination checks), `apps/api/tests/unit/conversion-quotes.test.ts`, `apps/api/tests/unit/asset-conversion.test.ts`, `apps/api/tests/unit/token-conversion-round.test.ts`, plus a read-only integration test against real mainnet quote/build behavior (`apps/api/tests/integration/soroswap-conversion.integration.test.ts` - never signs or sends)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| DeFi exit adapters                                                                                  | `apps/web/tests/unit/verify.test.ts` (the exit branch's contract/function pinning, plus the #208 sub-invocation cases: a hostile sub-invocation per protocol shape, the allowance-spend diversion case, the unrecognized-function and wrong-arity fail-closed cases, and the second-independent-authorization-root regression in `intent-serialize.test.ts`), `apps/api/tests/unit/{blend,aquarius,soroswap}-exit-adapter.test.ts`, `apps/api/tests/unit/exit-invariants.test.ts`, `apps/api/tests/unit/exit-round.test.ts`, `apps/api/tests/unit/contract-registry.test.ts` (wasmHash resolution, the unknown-hash refusal), `apps/api/tests/integration/{blend,aquarius,soroswap}-exit-adapter.integration.test.ts`. The Playwright e2e specs under `apps/web/tests/e2e/{blend,aquarius,soroswap,multi-protocol}-exit.spec.ts` exercise a real testnet close end to end and run nightly (`.github/workflows/e2e-nightly.yml`), not on every PR - treat them as a slower confirmation layer, not a PR-blocking one                                                                                                                                                                                                                   |

## 12. Maintenance

This document goes stale the same way `docs/architecture.md`'s trust boundary does: **a new close operation,
a new signing key, or a new adapter needs a STRIDE entry added here in the same pull request that introduces
it**, not as a follow-up. In particular:

* A new operation shape added to the builder and to `verify()`'s allowlist (CLAUDE.md, "Consequence for any
  new close operation") gets a row in Section 4's Elevation of privilege threat.
* A new server-side signing key gets its own subsection under Section 5/6's pattern before it ships, the same
  way this document covers the fee-bump sponsor ahead of its implementation.
* A new DeFi protocol adapter that introduces its own invariants (`docs/architecture.md` §9.9) is covered by
  those invariants directly; it only needs an entry here if it changes what the API signs or holds.

Section 6 was re-verified against the actual implementation and re-labeled "implemented" once #164 merged;
its coverage row in Section 11 is filled in. Before #165, the endpoint existed but nothing in the guided close
flow ever called it; #165 wired it in - `apps/web/hooks/useCloseExecution.ts` requests sponsorship and
re-verifies the result the same way it re-verifies the mediator's co-sign (Section 5) - and Section 11's
coverage row now lists the client-side test files this added.

Sections 7-9 (allowance revocation, Soroban token conversion, DeFi exit adapters) were added after this rule
was already being violated: epics #151 and #159 shipped their PRs (#212-#220) without the STRIDE entries this
section calls for, so this document briefly did not cover three real, already-signing surfaces. Section 9 in
particular was written only once issue #208 - a real gap in the DeFi-exit surface it now describes - was found
and fixed, rather than ahead of the surface's implementation the way this section asks for. Recorded here
plainly rather than silently backfilled, since a maintenance section that hides its own past lapses is worse
than one that names them.
