Stellar Smart Contracts
Guide for building Stellar smart contracts in Rust. Smart contracts on Stellar were formerly branded "Soroban" — the platform name is retired, but the Rust SDK (soroban-sdk) and several tool names keep the prefix.
This file covers setup and the core workflow. The deep dives live alongside it — read the file that matches the task:
| Task |
File |
| Storage/TTL, authorization, constructors, cross-contract calls, tokens, events, errors, upgrades, factories, governance/DeFi patterns, fees/resources, troubleshooting |
development.md |
| Unit, integration, fuzz, property, fork, and mutation testing |
testing.md |
| Security review, vulnerability classes, checklists, audit prep, tooling |
security.md |
When to use this skill
- Writing a Stellar smart contract in Rust
- Setting up contract tests (any layer)
- Reviewing a contract for security issues
- Architecting upgradeable contracts, factories, custom accounts, or DeFi primitives
- Debugging a contract-specific error (auth, storage, archival, resource limits)
Related skills
- Asset issuance, trustlines, and SAC deployment →
../assets/SKILL.md
- Frontend/wallets that call your contract →
../dapp/SKILL.md
- Chain data queries (RPC/Horizon) →
../data/SKILL.md
- ZK verification (BLS12-381, Groth16, Circom/Noir/RISC Zero) →
../zk-proofs/SKILL.md
- SEP/CAP standards and ecosystem links →
../standards/SKILL.md
Versions
This skill was written against protocol 27 (soroban-sdk v27, rs-soroban-env v27, stellar-cli v27). Version numbers in examples are illustrative — resolve the current ones from these sources rather than trusting any doc:
soroban-sdk major version tracks the protocol version (SDK 27 ↔ protocol 27). This rule outlives any specific release.
- Latest SDK release: crates.io/crates/soroban-sdk (or
cargo add soroban-sdk, which resolves it). Pre-releases (-rc.x) exist only during a protocol rollout and must be pinned with the exact version string; GitHub releases lists them with changelogs.
- Networks upgrade by validator vote, testnet before mainnet — pin the SDK major matching the network you deploy to. Live protocol version: RPC
getVersionInfo or Stellar Lab.
- Numeric network limits quoted here are mainnet settings at time of writing; they change by vote — Stellar Lab's Network Limits page and
stellar network settings --network mainnet show the live values.
Platform constraints
Contracts are Rust compiled to WebAssembly, run in a sandboxed host:
#![no_std] required — use soroban_sdk types (String, Vec, Map, Symbol), not the Rust standard library
- Compile for the
wasm32v1-none target (Rust ≥ 1.84) — the only Wasm target the Stellar runtime supports
- 128KB compiled contract size limit (network-configured)
Symbol is limited to 32 characters (a-zA-Z0-9_); symbol_short!() covers up to 9
- Storage is rented: every entry has a TTL and can be archived — see development.md
- No
delegatecall, and cross-contract reentrancy is blocked by the host — see security.md
- No I/O, no networking, no clock beyond the ledger timestamp — everything a contract can do hangs off
Env
Project setup
stellar contract init my-contract # scaffolds a Cargo workspace with contracts/
cd my-contract
rustup target add wasm32v1-none # once per toolchain
Cargo.toml essentials (what stellar contract init generates):
[lib]
crate-type = ["lib", "cdylib"] # lib is needed for tests and fuzzing
[dependencies]
soroban-sdk = "27" # protocol 27, which mainnet runs at the time of writing.
# Pre-releases (`-rc.x`) exist only during a rollout and need the
# exact version string. Check crates.io for the latest release, and
# the live protocol version of your target network, before deploying.
[dev-dependencies]
soroban-sdk = { version = "27", features = ["testutils"] } # match above
[profile.release]
opt-level = "z"
overflow-checks = true
debug = 0
strip = "symbols"
debug-assertions = false
panic = "abort"
codegen-units = 1
lto = true
Contract anatomy
One compact example showing state, constructor, auth, TTL, a typed error, and an event:
#![no_std]
use soroban_sdk::{
contract, contracterror, contractevent, contractimpl, contracttype, Address, Env,
};
#[contracttype]
#[derive(Clone)]
pub enum DataKey {
Admin,
Counter,
}
#[contracterror]
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[repr(u32)]
pub enum Error {
NotInitialized = 1,
}
// Emitted as topics ("incremented", by) with data {count} — #[topic] fields
// become topics, the rest go in the data payload.
#[contractevent]
pub struct Incremented {
#[topic]
pub by: Address,
pub count: u32,
}
#[contract]
pub struct CounterContract;
#[contractimpl]
impl CounterContract {
// Runs once, atomically, at deploy time. Must be named `__constructor`.
// Does not run again on upgrade.
pub fn __constructor(env: Env, admin: Address) {
env.storage().instance().set(&DataKey::Admin, &admin);
env.storage().instance().set(&DataKey::Counter, &0u32);
}
pub fn increment(env: Env) -> Result<u32, Error> {
let admin: Address = env
.storage()
.instance()
.get(&DataKey::Admin)
.ok_or(Error::NotInitialized)?;
admin.require_auth();
let count: u32 = env.storage().instance().get(&DataKey::Counter).unwrap_or(0);
let count = count + 1;
env.storage().instance().set(&DataKey::Counter, &count);
// Extend TTL so contract state is not archived (threshold, extend-to)
env.storage().instance().extend_ttl(120 * 17280, 180 * 17280);
Incremented { by: admin, count }.publish(&env);
Ok(count)
}
pub fn get_count(env: Env) -> u32 {
env.storage().instance().get(&DataKey::Counter).unwrap_or(0)
}
}
Full patterns (three storage types, auth variants, cross-contract calls, tokens, custom types): development.md.
Build, deploy, invoke
# Build optimized WASM → target/wasm32v1-none/release/*.wasm
# (optimization is on by default; --optimize=false to disable)
stellar contract build
# Create and fund an identity (testnet)
stellar keys generate alice --network testnet --fund
# Deploy (constructor args go after the `--`)
stellar contract deploy \
--wasm target/wasm32v1-none/release/my_contract.wasm \
--source-account alice \
--network testnet \
-- \
--admin alice
# Invoke
stellar contract invoke \
--id CONTRACT_ID \
--source-account alice \
--network testnet \
-- \
increment
To upload WASM without instantiating (e.g. for factories or upgrades), use stellar contract upload (the older stellar contract install is a deprecated alias).
Minimal test
// src/test.rs — included from lib.rs with `mod test;`
#![cfg(test)]
use super::*;
use soroban_sdk::{testutils::Address as _, Address, Env};
#[test]
fn test_increment() {
let env = Env::default();
env.mock_all_auths();
let admin = Address::generate(&env);
let contract_id = env.register(CounterContract, (admin.clone(),));
let client = CounterContractClient::new(&env, &contract_id);
assert_eq!(client.increment(), 1);
assert_eq!(client.get_count(), 1);
}
Auth mocking, event assertions, fuzzing, fork tests, and CI setup: testing.md.
Before mainnet
Work through the checklists in security.md — authorization, reinitialization, arithmetic, storage TTLs, and cross-contract validation are the recurring failure modes.
Documentation
1---2name: smart-contracts3description: Stellar smart contract development (Rust, soroban-sdk). Entry point with project setup, contract anatomy, and build/deploy workflow, routing to three companion files in this directory — development.md (storage/TTL, authorization, cross-contract calls, tokens, events, errors, upgrades, fees, troubleshooting), testing.md (unit, fuzz, property, fork, mutation, integration), and security.md (vulnerability classes, checklists, tooling, audits). Use when writing, testing, reviewing, securing, debugging, or shipping Stellar smart contracts, including anything the user calls "Soroban" — Soroban contracts, soroban-sdk, Soroban auth/storage/TTL errors, SEP-41 tokens, or SAC integration from contract code.4---56# Stellar Smart Contracts78Guide for building Stellar smart contracts in Rust. Smart contracts on Stellar were formerly branded "Soroban" — the platform name is retired, but the Rust SDK (`soroban-sdk`) and several tool names keep the prefix.910This file covers setup and the core workflow. The deep dives live alongside it — **read the file that matches the task**:1112| Task | File |13|------|------|14| Storage/TTL, authorization, constructors, cross-contract calls, tokens, events, errors, upgrades, factories, governance/DeFi patterns, fees/resources, troubleshooting | [development.md](development.md) |15| Unit, integration, fuzz, property, fork, and mutation testing | [testing.md](testing.md) |16| Security review, vulnerability classes, checklists, audit prep, tooling | [security.md](security.md) |1718## When to use this skill19- Writing a Stellar smart contract in Rust20- Setting up contract tests (any layer)21- Reviewing a contract for security issues22- Architecting upgradeable contracts, factories, custom accounts, or DeFi primitives23- Debugging a contract-specific error (auth, storage, archival, resource limits)2425## Related skills26- Asset issuance, trustlines, and SAC deployment → `../assets/SKILL.md`27- Frontend/wallets that call your contract → `../dapp/SKILL.md`28- Chain data queries (RPC/Horizon) → `../data/SKILL.md`29- ZK verification (BLS12-381, Groth16, Circom/Noir/RISC Zero) → `../zk-proofs/SKILL.md`30- SEP/CAP standards and ecosystem links → `../standards/SKILL.md`3132## Versions3334This skill was written against **protocol 27** (`soroban-sdk` v27, `rs-soroban-env` v27, `stellar-cli` v27). Version numbers in examples are illustrative — resolve the current ones from these sources rather than trusting any doc:3536- **`soroban-sdk` major version tracks the protocol version** (SDK 27 ↔ protocol 27). This rule outlives any specific release.37- Latest SDK release: [crates.io/crates/soroban-sdk](https://crates.io/crates/soroban-sdk) (or `cargo add soroban-sdk`, which resolves it). Pre-releases (`-rc.x`) exist only during a protocol rollout and must be pinned with the exact version string; [GitHub releases](https://github.com/stellar/rs-soroban-sdk/releases) lists them with changelogs.38- Networks upgrade by validator vote, testnet before mainnet — pin the SDK major matching the network you deploy to. Live protocol version: RPC `getVersionInfo` or [Stellar Lab](https://lab.stellar.org).39- Numeric network limits quoted here are mainnet settings at time of writing; they change by vote — [Stellar Lab's Network Limits page](https://lab.stellar.org/network-limits) and `stellar network settings --network mainnet` show the live values.4041## Platform constraints4243Contracts are Rust compiled to WebAssembly, run in a sandboxed host:4445- `#![no_std]` required — use `soroban_sdk` types (`String`, `Vec`, `Map`, `Symbol`), not the Rust standard library46- Compile for the **`wasm32v1-none`** target (Rust ≥ 1.84) — the only Wasm target the Stellar runtime supports47- 128KB compiled contract size limit (network-configured)48- `Symbol` is limited to 32 characters (`a-zA-Z0-9_`); `symbol_short!()` covers up to 949- Storage is rented: every entry has a TTL and can be archived — see [development.md](development.md#storage)50- No `delegatecall`, and cross-contract reentrancy is blocked by the host — see [security.md](security.md)51- No I/O, no networking, no clock beyond the ledger timestamp — everything a contract can do hangs off `Env`5253## Project setup5455```bash56stellar contract init my-contract # scaffolds a Cargo workspace with contracts/57cd my-contract58rustup target add wasm32v1-none # once per toolchain59```6061`Cargo.toml` essentials (what `stellar contract init` generates):6263```toml64[lib]65crate-type = ["lib", "cdylib"] # lib is needed for tests and fuzzing6667[dependencies]68soroban-sdk = "27" # protocol 27, which mainnet runs at the time of writing.69 # Pre-releases (`-rc.x`) exist only during a rollout and need the70 # exact version string. Check crates.io for the latest release, and71 # the live protocol version of your target network, before deploying.7273[dev-dependencies]74soroban-sdk = { version = "27", features = ["testutils"] } # match above7576[profile.release]77opt-level = "z"78overflow-checks = true79debug = 080strip = "symbols"81debug-assertions = false82panic = "abort"83codegen-units = 184lto = true85```8687## Contract anatomy8889One compact example showing state, constructor, auth, TTL, a typed error, and an event:9091```rust92#![no_std]93use soroban_sdk::{94 contract, contracterror, contractevent, contractimpl, contracttype, Address, Env,95};9697#[contracttype]98#[derive(Clone)]99pub enum DataKey {100 Admin,101 Counter,102}103104#[contracterror]105#[derive(Copy, Clone, Debug, Eq, PartialEq)]106#[repr(u32)]107pub enum Error {108 NotInitialized = 1,109}110111// Emitted as topics ("incremented", by) with data {count} — #[topic] fields112// become topics, the rest go in the data payload.113#[contractevent]114pub struct Incremented {115 #[topic]116 pub by: Address,117 pub count: u32,118}119120#[contract]121pub struct CounterContract;122123#[contractimpl]124impl CounterContract {125 // Runs once, atomically, at deploy time. Must be named `__constructor`.126 // Does not run again on upgrade.127 pub fn __constructor(env: Env, admin: Address) {128 env.storage().instance().set(&DataKey::Admin, &admin);129 env.storage().instance().set(&DataKey::Counter, &0u32);130 }131132 pub fn increment(env: Env) -> Result<u32, Error> {133 let admin: Address = env134 .storage()135 .instance()136 .get(&DataKey::Admin)137 .ok_or(Error::NotInitialized)?;138 admin.require_auth();139140 let count: u32 = env.storage().instance().get(&DataKey::Counter).unwrap_or(0);141 let count = count + 1;142 env.storage().instance().set(&DataKey::Counter, &count);143144 // Extend TTL so contract state is not archived (threshold, extend-to)145 env.storage().instance().extend_ttl(120 * 17280, 180 * 17280);146147 Incremented { by: admin, count }.publish(&env);148 Ok(count)149 }150151 pub fn get_count(env: Env) -> u32 {152 env.storage().instance().get(&DataKey::Counter).unwrap_or(0)153 }154}155```156157Full patterns (three storage types, auth variants, cross-contract calls, tokens, custom types): [development.md](development.md).158159## Build, deploy, invoke160161```bash162# Build optimized WASM → target/wasm32v1-none/release/*.wasm163# (optimization is on by default; --optimize=false to disable)164stellar contract build165166# Create and fund an identity (testnet)167stellar keys generate alice --network testnet --fund168169# Deploy (constructor args go after the `--`)170stellar contract deploy \171 --wasm target/wasm32v1-none/release/my_contract.wasm \172 --source-account alice \173 --network testnet \174 -- \175 --admin alice176177# Invoke178stellar contract invoke \179 --id CONTRACT_ID \180 --source-account alice \181 --network testnet \182 -- \183 increment184```185186To upload WASM without instantiating (e.g. for factories or upgrades), use `stellar contract upload` (the older `stellar contract install` is a deprecated alias).187188## Minimal test189190```rust191// src/test.rs — included from lib.rs with `mod test;`192#![cfg(test)]193use super::*;194use soroban_sdk::{testutils::Address as _, Address, Env};195196#[test]197fn test_increment() {198 let env = Env::default();199 env.mock_all_auths();200 let admin = Address::generate(&env);201 let contract_id = env.register(CounterContract, (admin.clone(),));202 let client = CounterContractClient::new(&env, &contract_id);203204 assert_eq!(client.increment(), 1);205 assert_eq!(client.get_count(), 1);206}207```208209Auth mocking, event assertions, fuzzing, fork tests, and CI setup: [testing.md](testing.md).210211## Before mainnet212213Work through the checklists in [security.md](security.md) — authorization, reinitialization, arithmetic, storage TTLs, and cross-contract validation are the recurring failure modes.214215## Documentation216217- [Smart contract docs](https://developers.stellar.org/docs/build/smart-contracts)218- [Example contracts](https://github.com/stellar/soroban-examples)219- [soroban-sdk API reference](https://docs.rs/soroban-sdk)220- [Stellar CLI manual](https://developers.stellar.org/docs/tools/cli/stellar-cli)