Soroban smart contracts for Vortex Protocol — intent-based cross-chain swaps settled on Stellar.
This repository holds the on-chain logic that guarantees settlement: intent
lifecycle, solver bonds, and slashing. Part of the multi-repo Vortex stack —
see also vortex-backend
and vortex-frontend.
Core protocol logic (intent_settlement/src/lib.rs):
submit_intent()— user creates a swap intentaccept_intent()— solver claims exclusive fill rightsfill_intent()— solver delivers output tokens to the usercancel_intent()— user cancels an open intentexpire_intent()— permissionless: materializes an unfilled intent's expiryslash_solver()— permissionless: slashes a solver that failed to fillregister_solver()/deregister_solver()/withdraw_bond()— solver bond managementset_fee_recipient()/transfer_admin()— admin key managementpause()/unpause()— admin-only incident responseadd_allowed_dst_token()/remove_allowed_dst_token()/set_dst_allowlist_enabled()— optional dst_token allowlistadd_allowed_src_chain()/remove_allowed_src_chain()/set_src_chain_allowlist_enabled()— optional src_chain allowlist (#34)rescue_tokens()— admin-only recovery of non-bond tokens accidentally sent to the contract (#35)
All examples use the Stellar CLI
against a deployed contract. Swap <CONTRACT_ID> and <SECRET_KEY> for your
deployment; addresses shown are placeholders.
# User submits a swap intent: 1 ETH on Ethereum for at least 3500 USDC on Stellar
stellar contract invoke --id <CONTRACT_ID> --source <SECRET_KEY> --network testnet -- \
submit_intent \
--user <USER_ADDRESS> \
--src_chain '"ethereum"' \
--src_token '"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"' \
--src_amount 1000000000000000000 \
--dst_token <USDC_SAC_ADDRESS> \
--min_dst_amount 35000000000
# Solver registers with a 50 USDC bond (MIN_BOND)
stellar contract invoke --id <CONTRACT_ID> --source <SOLVER_SECRET_KEY> --network testnet -- \
register_solver --solver <SOLVER_ADDRESS> --bond_amount 500000000
# Solver claims exclusive fill rights on an intent
stellar contract invoke --id <CONTRACT_ID> --source <SOLVER_SECRET_KEY> --network testnet -- \
accept_intent --solver <SOLVER_ADDRESS> --intent_id <INTENT_ID>
# Solver delivers the output and closes out the intent
stellar contract invoke --id <CONTRACT_ID> --source <SOLVER_SECRET_KEY> --network testnet -- \
fill_intent --solver <SOLVER_ADDRESS> --intent_id <INTENT_ID> --fill_amount 35000000000
# Anyone can slash a solver that accepted but missed the fill window
stellar contract invoke --id <CONTRACT_ID> --source <ANY_SECRET_KEY> --network testnet -- \
slash_solver --intent_id <INTENT_ID>
# Read-only: check current protocol stats
stellar contract invoke --id <CONTRACT_ID> --source <ANY_SECRET_KEY> --network testnet -- \
get_statsThe diagram below covers all six IntentState variants and the functions that
drive each transition.
stateDiagram-v2
[*] --> Open : submit_intent()
Open --> Accepted : accept_intent()\n[solver registered & active,\n deadline not reached]
Open --> Cancelled : cancel_intent()\n[caller == intent.user]
Open --> Expired : expire_intent()\n[now >= deadline]
Accepted --> Filled : fill_intent()\n[fill_amount >= min_dst_amount,\n now < deadline]
Accepted --> Open : slash_solver()\n[now >= deadline]\n(10 % bond slashed,\nintent re-opened with fresh deadline)
Filled --> [*]
Cancelled --> [*]
Expired --> [*]
Note:
accept_intentalso lazily sets state toExpired(and panics) when the intent's original deadline has already passed, but this is a read-time guard, not a persistedOpen → Expiredpath — that explicit transition is handled byexpire_intent.
The table below maps every Error variant to the function(s) that raise it and
the exact condition that triggers it.
| # | Variant | Raised by | Condition |
|---|---|---|---|
| 1 | AlreadyInitialized |
initialize |
DataKey::Admin already exists in instance storage |
| 2 | Unauthorized |
fill_intent, cancel_intent |
Caller is not the assigned solver / intent owner |
| 3 | IntentNotFound |
accept_intent, fill_intent, cancel_intent, slash_solver, expire_intent |
No IntentRecord found for the supplied intent_id |
| 4 | IntentNotOpen |
cancel_intent, expire_intent |
Intent state is not Open |
| 5 | IntentExpired |
accept_intent |
now >= intent.deadline when a solver tries to accept |
| 6 | IntentNotAccepted |
fill_intent, slash_solver |
Intent state is not Accepted |
| 7 | SolverNotRegistered |
accept_intent, deregister_solver, withdraw_bond |
No SolverRecord found for the address |
| 8 | SolverBondTooLow |
register_solver, withdraw_bond |
Resulting bond total < MIN_BOND (50 USDC) |
| 9 | InsufficientOutput |
fill_intent |
fill_amount < intent.min_dst_amount |
| 10 | FillWindowExpired |
fill_intent |
now >= intent.deadline (fill window elapsed); also used in slash_solver as an inverse guard (window not yet expired) |
| 11 | CannotCancelAccepted |
cancel_intent |
Intent state is Accepted |
| 12 | SolverInactive |
accept_intent |
solver_record.is_active == false |
| 13 | ZeroAmount |
submit_intent, register_solver, withdraw_bond |
src_amount ≤ 0, min_dst_amount ≤ 0, or bond_amount ≤ 0 |
| 14 | InvalidDeadline |
submit_intent |
Supplied deadline ≤ env.ledger().timestamp() |
| 15 | IntentAlreadyFilled |
fill_intent |
Intent state is Filled |
| 16 | NotInitialized |
set_fee_recipient, transfer_admin, require_admin |
DataKey::Admin absent (contract not initialized) |
| 17 | SolverHasActiveIntents |
deregister_solver |
solver_record.active_intents > 0 |
| 18 | ContractPaused |
submit_intent, accept_intent, fill_intent (via require_not_paused) |
DataKey::Paused is true |
| 19 | DeadlineNotReached |
expire_intent |
now < intent.deadline |
| 20 | InsufficientBond |
withdraw_bond |
Requested withdrawal amount > solver_record.bond_amount |
| 21 | DstTokenNotAllowed |
submit_intent |
DstAllowlistEnabled is true and dst_token is not in the AllowedDstToken list |
Tiered solver staking with reputation scores. See the roadmap below.
- Rust 1.78+ with the
wasm32-unknown-unknowntarget - Stellar CLI
cd intent_settlement
cargo fmt --all
cargo clippy --all-targets -- -D warnings
cargo test
stellar contract buildstellar contract deploy \
--wasm target/wasm32-unknown-unknown/release/vortex_intent_settlement.wasm \
--source <SECRET_KEY> \
--network testnetSettlement relies on two primitives:
- Solver bonds — solvers lock USDC to participate. Failed fills slash 10% of their bond, making repeated failures unprofitable.
- Fill-window enforcement — once a solver accepts, the intent is locked for
5 minutes. If they fail to fill, the intent reverts to
openand is re-auctioned, and the bond is slashed permissionlessly viaslash_solver().
pause() halts submit_intent, accept_intent, fill_intent, and the
solver bond management functions (register_solver, deregister_solver,
withdraw_bond). The rationale:
- During a live incident an admin needs to freeze the full protocol state to investigate. Allowing solvers to withdraw bonds while paused would let them shed collateral exactly when the protocol needs it most as a backstop.
slash_solver()remains permissionless and unpauseable — a solver who already accepted an intent cannot dodge accountability by waiting out the pause.cancel_intent()remains open during a pause — users should always be able to reclaim their Open intents without needing admin cooperation.
is_dst_allowlist_enabled defaults to false on a fresh deployment,
meaning submit_intent accepts any dst_token address until an admin opts in.
Pre-launch action required: before going live on mainnet, call
add_allowed_dst_token() for every supported output token, then call
set_dst_allowlist_enabled(true) to enforce validation. This prevents users
from accidentally targeting an unsupported or malicious token contract.
The same pattern applies to the source-chain allowlist (is_src_chain_allowlist_enabled,
also off by default). Call add_allowed_src_chain() for every supported source
chain (e.g. "ethereum", "base", "polygon"), then enable enforcement with
set_src_chain_allowlist_enabled(true).
To report a vulnerability, see the org
SECURITY.md.
For the detailed threat model specific to intent_settlement, see
SECURITY.md in this repository.
- Contract test suite —
soroban_sdktestutils coverage for the full intent lifecycle, solver bonding/slashing, admin controls, pause, and storage TTL management - Solver registry contract — tiered staking, reputation NFT, dispute resolution
- Cross-chain proof verification — verify source-chain tx on-chain via Stellar oracle / messaging infra
See the org-wide CONTRIBUTING.md.
MIT © 2025–2026 Vortex Protocol Contributors