For general vulnerability reporting instructions, see the org-level SECURITY.md.
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.
| 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 |
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.
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.
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.
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.
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).
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.
| 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.
- 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.
-
Hardware wallet required. The admin signing key must never exist only in software (e.g. a
.envfile, a CI secret, or an in-memory key). Use a Ledger, Trezor, or equivalent hardware device for all admin transactions. -
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.
-
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.
-
Pre-launch checklist. Before enabling mainnet enforcement:
- Populate
add_allowed_src_chainwith every supported source chain. - Call
set_src_chain_allowlist_enabled(true). - Populate
add_allowed_dst_tokenfor every supported output token. - Call
set_dst_allowlist_enabled(true). - Confirm
get_admin()andget_fee_recipient()return the expected multisig addresses. - See
docs/pre-deploy-security-checklist.mdfor the full checklist.
- Populate
-
Monitor fee recipient.
set_fee_recipient(via the two-steppropose_fee_recipient/accept_fee_recipientflow) redirects all protocol fee and slash proceeds. Monitor thefee_recipient_proposedandfee_recipient_updatedevents on-chain; any unexpected proposal should be treated as a potential compromise. -
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. -
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_adminand notify all registered solvers.
| 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).
- 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_solverthe intent is reset toOpenwith a freshINTENT_EXPIRYdeadline. There is currently no cap on how many times an intent can cycle throughOpen → Accepted → Slashed. - No allowlist by default. Until an admin calls
set_dst_allowlist_enabled(true), any token address — including malicious contracts — can be used asdst_token.
Please do not open a public GitHub issue for security vulnerabilities. Follow the responsible-disclosure process described in the org-level SECURITY.md.