Soroban smart contracts for the Vatix prediction market protocol on Stellar.
Core smart contracts powering Vatix prediction markets, written in Rust for the Stellar Soroban platform.
| Contract | Crate | Status | Description |
|---|---|---|---|
| Market | contracts/market |
✅ Complete | Market creation, position trading, oracle resolution, and settlement |
| Treasury | contracts/treasury |
✅ Complete | Protocol fee collection from withdrawal events; admin-controlled fee withdrawal |
| Outcome Token | contracts/outcome-token |
✅ Complete | Fungible SAC-compatible tokens representing YES/NO market outcomes |
| Resolution | contracts/resolution |
✅ Complete | Standalone oracle-based outcome resolution with dispute window |
The Market contract can optionally wire supporting modules via admin-configured contract addresses. Once registered:
set_treasuryregisters a Treasury contract address that receives fee deposits fromwithdraw_unused_collateral.set_outcome_token_contractregisters an Outcome Token contract that mints/burns tokens when market positions change.set_resolution_contractregisters a Resolution contract that gatesresolve_marketuntil a candidate is finalized.- When configured,
withdraw_unused_collateralcomputes a fee, transfers it to the Treasury, and records it viacollect_fee.
- Market Contract: Market creation, trading, and settlement logic
- Treasury: Fee collection and protocol management
- Outcome Token: Mint/burn YES/NO outcome share tokens
- Resolution Contract: Challenge-window lifecycle for oracle resolution candidates
See docs/cross-contract-call-graph.md for the full edge-by-edge call graph, authorization requirements, and registration prerequisites.
- Language: Rust
- Platform: Stellar Soroban
- Testing: Soroban SDK test utilities
- Build: Cargo
The Market contract uses storage versioning to ensure data integrity across upgrades. See comprehensive documentation:
-
Storage Migration Guide - Complete guide for handling storage version bumps, including:
- Reviewer checklist for
StorageKeytable drift - how to verify theStorageKeyenum and thelib.rsstorage table stay in sync - When to bump storage version
- Step-by-step migration procedures (testnet & mainnet)
- Testing strategies
- Rollback and recovery procedures
- Common pitfalls and solutions
- Reviewer checklist for
-
Migration History - Specific changes and data migration notes
Quick Reference:
- Current storage version:
3 - Always bump version for: field changes, type changes, semantic changes to stored data
- Migration is required when deploying with a new storage version
- Binary outcome markets (Yes/No)
- Share minting and trading
- Oracle-based resolution
- Fee distribution
- Market expiration and settlement
Allows administrators to prevent new collateral deposits into a market while preserving all other functionality (trading, withdrawals, and settlement).
Use Cases:
- Prevent new positions when approaching market expiration
- Lock down markets during resolution or dispute windows
- Manage market liquidity and exposure
API:
pub fn close_market_to_deposits(env: Env, admin: Address, market_id: u32) -> Result<(), ContractError>Event:
MarketClosedToDepositsEvent {
market_id: u32,
admin: Address,
closed_at: u64,
}
For detailed documentation, see CLOSE_MARKET_FEATURE.md.
The Market Contract still owns the final resolve_market(market_id, outcome, signature) state transition. The separate Resolution Contract adds the missing on-chain challenge window that mirrors the backend ResolutionCandidate flow:
propose(proposer, market_id, outcome, signature, evidence_uri, challenge_window_seconds)stores a signed candidate and publishes itschallenge_deadline.challenge(challenger, candidate_id, challenge_uri)can be called until the deadline. A challenged candidate cannot be finalized.finalize(finalizer, candidate_id)succeeds only after the challenge window closes and returns the candidate payload.- The backend or registered factory then submits the finalized candidate to
MarketContract::resolve_market, using the stored outcome and oracle signature.
contracts/resolution is intentionally a lifecycle and registration layer, not a replacement settlement engine. initialize(admin, factory, market_contract) registers the factory/market relationship so off-chain services can discover which resolution contract guards a market deployment.
For the complete, up-to-date field reference across all four contracts (Market, Treasury, Resolution, Outcome Token), see
docs/events-reference.md— the canonical schema reference for off-chain indexers. The table below covers a subset of Market events for a quick overview.
The Market Contract emits the following events for off-chain indexing and tracking:
| Event | Topics | Fields | Description |
|---|---|---|---|
contract_initialized |
admin |
initialized_at: u64 |
Emitted when the contract is initialized with an admin |
market_created |
market_id |
creator: Address, question: String, end_time: u64 |
Emitted when a new market is created |
collateral_deposited |
user, market_id |
amount: i128, new_total: i128 |
Emitted when a user deposits collateral into a market |
collateral_withdrawn |
user, market_id |
amount: i128, new_total: i128 |
Emitted when a user withdraws collateral from a market |
position_updated |
market_id, user |
yes_shares: i128, no_shares: i128, locked_collateral: i128 |
Emitted when a user's position is updated after trading |
trade_executed |
market_id, user |
quantity: i128, price_bps: i128, side_yes: bool, executed_at: u64 |
Emitted when a user executes a trade (buy or sell) |
position_limit_exceeded |
market_id, user |
side_yes: bool |
Emitted when a trade would result in negative shares |
market_resolved |
market_id |
resolver: BytesN<32>, outcome: bool, resolved_at: u64 |
Emitted when a market is resolved with an oracle-signed outcome |
position_settled |
market_id, user |
payout: i128, settled_at: u64 |
Emitted when a user's position is settled and payout is transferred |
oracle_signature_verified |
market_id |
outcome: bool, verified_at: u64 |
Emitted when an oracle signature is verified during resolution |
fee_calculated |
market_id, user |
fee_amount: i128, available_after_fee: i128 |
Emitted when a fee is calculated during withdrawal |
validation_failed |
context |
error_code: u32 |
Emitted when validation fails, recording context and error code |
Off-chain indexers can efficiently filter events using the topic indices:
- By Market: Subscribe to events with
market_idtopic to track all activity in a specific market - By User: Subscribe to events with
usertopic to track all activity for a specific user - By Trade: Listen for
trade_executedto capture all trades with quantity, price, and side information
Next.js 16 app for prediction-market UI (mock data + Freighter wallet stub).
pnpm install
pnpm dev # http://localhost:3002
pnpm build:web- Rust toolchain (stable, with
wasm32-unknown-unknowntarget) - Stellar CLI (v21.4.0+) - Install from stellar.org/docs/tools/cli
- Node 20+ and pnpm 8+ (for web app and scripts)
# Install Rust and add WASM target
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
rustup target add wasm32-unknown-unknown
# Install Stellar CLI (macOS/Linux)
# See https://developers.stellar.org/docs/tools/cli for other platforms
curl -L https://github.com/stellar/stellar-cli/releases/download/v21.4.0/stellar-cli-21.4.0-x86_64-unknown-linux-gnu.tar.gz | tar xz
sudo mv stellar /usr/local/bin/
# Verify installation
stellar --versionAll contract builds use the canonical command: stellar contract build.
This is the single source of truth across Makefile, CI, and deployment scripts to ensure byte-for-byte identical WASM artifacts. The output path is always:
target/wasm32v1-none/release/<contract-name>.wasm
# Prerequisites: Rust toolchain, Soroban CLI
cd contracts/market && cargo build
cd ../treasury && cargo build
cd ../outcome-token && cargo build
cd ../resolution && cargo buildCI runs cargo check --workspace --all-targets in one job (.github/workflows/ci.yml) so a break in any single crate (contracts/market, contracts/treasury, contracts/resolution, contracts/outcome-token) fails the build immediately, even if that crate isn't otherwise touched by the PR. Run it locally before pushing:
cargo check --workspace --all-targetsGenerate 375 onboarding issues (125 per repo) — see scripts/issues/README.md.
pnpm issues:generate
pnpm issues:publish # requires gh authAll deployment scripts use the canonical stellar contract build command to ensure artifact consistency.
Builds and deploys the contract to Stellar testnet using the unified build toolchain.
What it does:
- Builds the contract using
stellar contract build(same as Makefile and CI) - Locates the WASM artifact at
target/wasm32v1-none/release/*.wasm - Deploys via
stellar contract deploy --wasm <path> --network testnet - Outputs the contract ID for downstream use
# Set credentials
export TESTNET_SECRET_KEY="S..."
# Deploy to testnet (uses stellar contract build internally)
bash scripts/deploy-testnet.shEnvironment variables:
TESTNET_SECRET_KEY(required) - Funded testnet account secret keySOROBAN_NETWORK(optional) - Network name (default:testnet)CONTRACT_DIR(optional) - Contract to build/deploy (default:contracts/market)WASM_PATH(optional) - Explicit WASM path override
Generic deployment script for any configured network.
# Deploy to testnet
bash scripts/deploy.shRequires Stellar CLI and a funded account. Set
SOROBAN_NETWORKandSOROBAN_ACCOUNTenv vars before running.
deployments/testnet.json (schema documented in deployments/README.md) is the single source of truth for testnet contract IDs, network passphrase, and RPC URL. After deploying a contract, record its ID there so apps/web and the scripts below can pick it up. Until a real deploy happens, contractId fields are empty-string placeholders.
scripts/testnet-smoke.sh performs a read-only, simulate-only invocation (stellar contract invoke --send=no) against a deployed Market contract on testnet, to confirm it's reachable — no signing or secret key is required.
pnpm testnet:smoke
# or
bash scripts/testnet-smoke.shWhat it needs:
- The
stellarCLI on PATH - A market contract ID, resolved from (in order): the
MARKET_CONTRACT_IDenv var, or.contracts.market.contractIdindeployments/testnet.json - RPC URL / network passphrase, resolved from
SOROBAN_RPC_URL/NETWORK_PASSPHRASEenv vars, else the registry file, else the default Stellar testnet values (https://soroban-testnet.stellar.org,Test SDF Network ; September 2015)
If no contract ID is configured, or the stellar CLI isn't installed, the script prints a message and exits 0 (guard mode) instead of failing — it's safe to run on a fresh checkout before any testnet deployment exists.
Freighter is not needed here. Freighter (the browser wallet) is only required for signed testnet interactions from the web app; this script never signs or submits a transaction.
To verify your local WASM matches what CI produces, compare hashes:
# Build locally
cd contracts/market
stellar contract build
# Compute and verify hash
bash ../../scripts/verify-wasm-hash.sh contracts/market
# Or use the Makefile target
make verifyThe script outputs the SHA256 hash of the WASM artifact. Compare this with:
- CI build artifacts (download from GitHub Actions)
- Builds from other developers
- Previously deployed contract hashes
Identical hashes confirm the build is reproducible across environments.
Artifact mismatch risks:
- ❌ Local testing with one WASM, deploying another
- ❌ CI tests passing but deployed contract failing
- ❌ Inability to reproduce production builds
With unified stellar contract build:
- ✅ Same WASM locally, in CI, and deployed
- ✅ Reproducible builds across environments
- ✅ Confidence that tested code is deployed code
Unified Build Command: All contracts use stellar contract build as the canonical build command.
This ensures:
- ✅ Identical artifacts across local builds, CI, and deployments
- ✅ Optimized WASM for Soroban runtime
- ✅ No drift between development and production builds
# Build any contract
cd contracts/market
stellar contract build
# Or use the Makefile convenience target
make buildThe Makefile, CI workflow (.github/workflows/ci.yml), and deployment scripts (scripts/deploy-testnet.sh) all use this same command to guarantee artifact consistency.
All builds output to:
target/wasm32v1-none/release/<contract-name>.wasm
Example artifacts:
vatix_market_contract.wasmvatix_treasury_contract.wasmvatix_outcome_token_contract.wasmvatix_resolution_contract.wasm
The web app consumes auto-generated TypeScript bindings for each contract, committed under apps/web/lib/contracts/. Regenerate them locally whenever a contract's public interface changes:
pnpm build:bindingsPrerequisites:
- Stellar CLI v21+ on PATH
- Rust
wasm32-unknown-unknownandwasm32v1-nonetargets installed
This runs scripts/generate-bindings.ts, which builds every contract to WASM and generates fresh TypeScript clients into apps/web/lib/contracts/. Commit the resulting changes alongside your contract change.
The frontend CI job regenerates bindings on every push/PR and fails with git diff --exit-code if the committed output under apps/web/lib/contracts/ differs from what was just generated — so stale bindings will block CI until you run pnpm build:bindings and commit the diff.
The scripts/ directory contains utility scripts for deployment, invocation, and contributor issue generation. Full documentation is in scripts/issues/README.md.
Smoke-tests a deployed contract by invoking one of its functions via the Soroban CLI. Used in CI to verify that the contract binary is callable after deployment.
CONTRACT_ID=your_contract_id bash scripts/invoke-example.shNote: Currently an echo guard. Replace with a real
stellar contract invokecall once the contract is deployed to a target network — see the TODO comment in the script.
The contracts/market/Makefile provides convenience targets for day-to-day contract work.
| Target | Description |
|---|---|
build |
Canonical build: Compiles using stellar contract build to produce optimized WASM |
test |
Run all unit and integration tests (depends on build) |
fmt |
Format all Rust source with cargo fmt --all |
clean |
Remove build artefacts via cargo clean |
# From the repo root
cd contracts/market
make # default — builds WASM using stellar contract build
make test # build then run the full test suite
make fmt # auto-format source files
make clean # wipe target/ directoryThe build target uses stellar contract build, which is the same command used by:
- CI pipeline (
.github/workflows/ci.yml) - Deployment scripts (
scripts/deploy-testnet.sh) - TypeScript bindings generation (
scripts/generate-bindings.ts)
This unified approach prevents artifact mismatches and ensures the WASM built locally is byte-for-byte identical to what's deployed and tested in CI.
Clippy is Rust's official linter and is enforced in CI. All warnings are treated as hard errors via -D warnings, so the build fails if any lint fires.
# Run from the contract directory
cd contracts/market
cargo clippy -- -D warningsTo suppress a lint where it is intentionally acceptable, add a targeted attribute in the source rather than weakening the global flag:
#[allow(clippy::lint_name)]
fn my_function() { ... }The CI step is defined in .github/workflows/ci.yml and runs automatically on every push and pull request.
Smart contract security is critical. All contracts will undergo:
- Extensive unit testing
- Integration testing
- External audits before mainnet deployment
See CONTRIBUTING.md for workspace layout, Soroban contract patterns, and the build/lint/test commands to run before opening a PR. For broader project information, check out vatix-docs.
MIT License
Part of the Vatix Protocol