Skip to content

Security: stellar-vortex-protocol/vortex-contracts

Security

SECURITY.md

Security Policy — vortex-contracts

For general vulnerability reporting instructions, see the org-level SECURITY.md.


Threat Model — intent_settlement

This document describes the trust assumptions, assets at risk, and known limitations of the intent_settlement contract ahead of a mainnet deployment. It is intended for auditors and integrators.


Assets at Risk

Asset Where held Value
Solver bonds intent_settlement contract account (USDC) ≥ 50 USDC per solver
User swap output Solver's token balance (transferred in fill_intent) Unbounded
Protocol fees FeeRecipient address 0.05 % of filled volume
Admin privileges Admin key in instance storage Full contract control

Trust Assumptions

1. Solver self-reporting

The contract has no on-chain proof that a solver actually sent tokens on the source chain. fill_intent trusts the solver to transfer dst_token to the user on Stellar; the economic incentive not to default is the bond slash (slash_solver takes 10 % of the bond permissionlessly once the fill window expires).

Implication: A solver with a large enough bond can accept an intent, fail to fill, absorb the 10 % slash, and profit if the spread on the source-chain trade is more valuable than 10 % of their bond. The bond size is the primary economic deterrent.

2. Admin key custody

A single Admin address controls:

  • pause / unpause — can halt all new swaps.
  • set_fee_recipient — redirects fee and slash proceeds.
  • transfer_admin — rotates the admin key (requires both old and new admin to sign).
  • add_allowed_dst_token / set_dst_allowlist_enabled — controls which tokens users can target.

Implication: A compromised admin key can grief the protocol (pause, fee redirection) but cannot steal user funds or solver bonds directly, because no admin function transfers tokens out of user or solver custody. Key rotation requires dual authorization, reducing the single-key-failure surface.

3. Destination token allowlist (default: off)

By default, submit_intent accepts any dst_token address. If an admin enables the allowlist (set_dst_allowlist_enabled(true)), only pre-approved tokens may be used as destinations.

Implication (allowlist disabled): A user or solver could name a malicious contract as dst_token. The risk lands on the user who submitted the intent — they would receive tokens from a contract they did not vet. Enabling the allowlist before mainnet is strongly recommended.

Implication (allowlist enabled): The allowlist is stored in instance storage, so adding/removing tokens is cheap but centralized under the admin key.

4. Fill-window timing

The fill window (FILL_WINDOW = 300 s) starts from accept_intent. The deadline stored on-chain is now + 300. A solver and a block producer colluding to manipulate ledger().timestamp() could extend the fill window slightly, but Stellar's timestamp drift is bounded by consensus rules and is not a meaningful attack surface in practice.

5. Permissionless slash and expire

slash_solver and expire_intent are callable by anyone. This is intentional — it prevents a solver from dodging accountability by waiting out a pause or ensuring only friendly callers settle the intent. A griefing scenario where someone repeatedly calls expire_intent on an already-expired intent is defended by the IntentNotOpen guard (idempotent after first call).



Admin Key Operational Security (#122)

The Admin address is the single most sensitive key in the protocol. It controls pause/unpause, fee routing, the destination-token allowlist, the source-chain allowlist, and admin rotation. A compromised admin key can halt the protocol and redirect all fee and slash proceeds. The recommendations below apply before and after mainnet launch.

Recommended custody model

Deployment stage Recommended setup
Testnet / staging Developer EOA on a hardware wallet (Ledger/Trezor)
Mainnet launch 2-of-3 or 3-of-5 multisig (e.g. Gnosis Safe equivalent on Stellar, or a Stellar multi-sig account with threshold set appropriately)
Long-term mainnet Hardware-backed multisig with geographically distributed key holders; a minimum M-of-N threshold of M ≥ 2

A design for a multisig admin wrapper is tracked in docs/114-multisig-admin-design.md. Until that wrapper is deployed, at minimum the admin key must reside on a hardware wallet — never a hot wallet or a key stored in plaintext.

Key rotation

  • Rotate the admin key with transfer_admin. Both the outgoing and incoming admin must sign the same transaction, so a typo'd address can never accidentally brick the contract.
  • After rotation, verify get_admin() returns the expected new address on-chain before discarding the old key material.
  • Rotation should be rehearsed on testnet before any live key handover.

Operational hygiene

  1. Hardware wallet required. The admin signing key must never exist only in software (e.g. a .env file, a CI secret, or an in-memory key). Use a Ledger, Trezor, or equivalent hardware device for all admin transactions.

  2. Separate key per environment. Testnet, staging, and mainnet must each have a distinct admin key. Reusing a key across environments means a compromise on a lower-trust environment immediately threatens mainnet.

  3. Minimal admin transactions. Admin calls should originate from an air-gapped or dedicated signing machine. Never sign admin transactions from a machine used for general web browsing or software development.

  4. Pre-launch checklist. Before enabling mainnet enforcement:

    • Populate add_allowed_src_chain with every supported source chain.
    • Call set_src_chain_allowlist_enabled(true).
    • Populate add_allowed_dst_token for every supported output token.
    • Call set_dst_allowlist_enabled(true).
    • Confirm get_admin() and get_fee_recipient() return the expected multisig addresses.
    • See docs/pre-deploy-security-checklist.md for the full checklist.
  5. Monitor fee recipient. set_fee_recipient (via the two-step propose_fee_recipient / accept_fee_recipient flow) redirects all protocol fee and slash proceeds. Monitor the fee_recipient_proposed and fee_recipient_updated events on-chain; any unexpected proposal should be treated as a potential compromise.

  6. Pause is not a long-term solution. pause() is an incident-response tool, not a substitute for fixing a bug. A paused contract still holds solver bonds; resolve the incident and unpause as quickly as safely possible to minimise disruption to solvers and users.

  7. Post-incident key rotation. Any time there is evidence that the admin key may have been exposed (phishing, malware, shoulder-surfing), rotate immediately via transfer_admin and notify all registered solvers.

What a compromised admin key can and cannot do

Can do Cannot do
Pause the contract (halt new swaps) Transfer user funds directly
Redirect future fees and slashes via propose_fee_recipient Drain solver bonds
Add/remove tokens from dst and src allowlists Cancel or fill intents on behalf of others
Rotate itself to a new admin address Bypass the solver bond slash (still permissionless)
Disable allowlist enforcement Recover already-transferred tokens

The blast radius of a compromised key is griefing and fee theft, not direct fund theft. Solver bonds and in-flight intent outputs are only moved by fill_intent, slash_solver, and rescue_tokens — none of which are callable unilaterally by the admin without other protocol preconditions being met first (except rescue_tokens, which is limited to non-bond, non-active-intent tokens).


Known Limitations

  • No cross-chain proof. The contract cannot verify the source-chain transaction. Trust is economic, not cryptographic.
  • Single admin key. There is no multi-sig or timelock on admin operations. Protocol operators should secure the admin key with a hardware wallet or multi-sig wrapper before mainnet.
  • Bond slash is fixed at 10 %. A solver with a very large bond can default cheaply. A dynamic slash proportional to intent size is on the roadmap.
  • Intent re-open after slash. After slash_solver the intent is reset to Open with a fresh INTENT_EXPIRY deadline. There is currently no cap on how many times an intent can cycle through Open → Accepted → Slashed.
  • No allowlist by default. Until an admin calls set_dst_allowlist_enabled(true), any token address — including malicious contracts — can be used as dst_token.

Reporting a Vulnerability

Please do not open a public GitHub issue for security vulnerabilities. Follow the responsible-disclosure process described in the org-level SECURITY.md.

There aren't any published security advisories