rs-soroban-sdk
Soroban SDK Skill
Soroban SDK is the Rust SDK for building smart contracts on the Stellar blockchain's Wasm-powered Soroban runtime.
Prerequisites
- Rust: v1.84.0 or higher
- Target: Install with
rustup target add wasm32v1-none - Stellar CLI: v25.1.0+ (recommended for building and testing)
- Install:
curl -fsSL https://github.com/stellar/stellar-cli/raw/main/install.sh | sh - Or:
brew install stellar-cli
- Install:
⚠️ Security First
Smart contracts handle valuable assets. Follow these rules to prevent vulnerabilities:
Do:
- ✅ Call
require_auth()before any state changes - ✅ Validate all inputs (amounts, addresses, array lengths)
- ✅ Use checked arithmetic (
.checked_add(),.checked_mul()) - ✅ Extend TTL on all persistent/instance storage writes
- ✅ Initialize contract only once with a guard flag
- ✅ Test authorization, overflow, and edge cases
Don't:
- ❌ Skip authorization checks
- ❌ Use unchecked arithmetic (can overflow/underflow)
- ❌ Allow reinitialization
- ❌ Forget to extend TTL on storage writes
- ❌ Trust external addresses without validation
See references/security.md for complete security guidance.
Core Contract Structure
Every Soroban contract follows this pattern:
#![no_std] // Required: excludes Rust std library (too large for contracts)
use soroban_sdk::{contract, contractimpl, Env};
#[contract]
pub struct MyContract;
#[contractimpl]
impl MyContract {
pub fn function_name(env: Env, param: Type) -> ReturnType {
// Implementation
}
}
Key requirements:
#![no_std]- Must be first line (standard library not available)- All contracts export as a single contract when compiled to WASM
- Function names max 32 characters
- Contract inputs must not be references
Key attributes:
#[contract]- Marks the struct as a contract type#[contractimpl]- Exports public functions as contract functions#[contracttype]- Converts custom types to/fromValfor storage#[contracterror]- Defines error enums withrepr(u32)#[contractevent]- Marks structs as publishable events
Environment (Env)
The Env type provides access to the contract execution environment. It's always the first parameter in contract functions.
pub fn my_function(env: Env) {
// Access storage
env.storage().persistent();
env.storage().temporary();
env.storage().instance();
// Get contract address
let contract_id = env.current_contract_address();
// Get ledger info
let ledger = env.ledger().sequence();
let timestamp = env.ledger().timestamp();
}
Storage Types
Soroban provides three storage types with different lifetimes and costs. See references/storage.md for detailed patterns.
Quick reference:
Persistent- Long-lived data (user balances, state)Temporary- Short-lived data (caching, temporary locks)Instance- Contract-wide configuration/metadata
Data Types
Core Types
Address- Universal identifier (contracts or accounts)Symbol- Short strings with limited charset (max 32 chars)Vec<T>- Growable array typeMap<K, V>- Ordered key-value dictionaryBytes- Growable byte arrayBytesN<N>- Fixed-size byte arrayString- UTF-8 string typeU256,I256- 256-bit integers
Type Macros
vec![&env, item1, item2]- Create Vecmap![&env, (key1, val1), (key2, val2)]- Create Mapsymbol_short!("text")- Create Symbol constantbytes!(&env, 0x010203)- Create Bytesbytesn!(&env, 0x010203)- Create BytesN
Authorization
⚠️ Critical: Authorization vulnerabilities are the #1 cause of smart contract exploits. Always call require_auth() before any state changes.
When a function requires authorization, use Address::require_auth():
pub fn transfer(env: Env, from: Address, to: Address, amount: i128) {
from.require_auth(); // ✅ ALWAYS FIRST
// Now authorized to proceed
}
Common mistake: Authorizing the wrong address
// ❌ WRONG: Authorizing recipient
pub fn transfer(env: Env, from: Address, to: Address, amount: i128) {
to.require_auth(); // Anyone can receive!
}
// ✅ CORRECT: Authorize sender
pub fn transfer(env: Env, from: Address, to: Address, amount: i128) {
from.require_auth(); // Sender must approve
}
For custom auth logic, see references/auth.md.
Testing
Use testutils feature for testing. Tests use Env::default() and register contracts:
#[test]
fn test() {
let env = Env::default();
let contract_id = env.register(MyContract, ());
let client = MyContractClient::new(&env, &contract_id);
let result = client.my_function(¶m);
assert_eq!(result, expected);
}
For advanced testing patterns, see references/testing.md.
Tokens
Work with tokens using the token module:
use soroban_sdk::token::{TokenClient, StellarAssetClient};
pub fn use_token(env: Env, token_address: Address, amount: i128) {
let token = TokenClient::new(&env, &token_address);
token.transfer(&from, &to, &amount);
}
See references/tokens.md for token integration patterns.
Events and Logging
Publish events for off-chain tracking:
env.events().publish((symbol_short!("transfer"), from, to), amount);
During development, use logging:
use soroban_sdk::log;
log!(&env, "Debug message: {}", value);
Error Handling
Define custom errors with #[contracterror]:
#[contracterror]
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[repr(u32)]
pub enum Error {
InvalidAmount = 1,
Unauthorized = 2,
InsufficientBalance = 3,
}
Use with panic_with_error! or assert_with_error!:
// Validate before operations
assert_with_error!(&env, amount > 0, Error::InvalidAmount);
assert_with_error!(&env, balance >= amount, Error::InsufficientBalance);
// Or panic directly
if amount == 0 {
panic_with_error!(&env, Error::InvalidAmount);
}
⚠️ Security: Always validate inputs to prevent:
- Integer overflow/underflow (use checked arithmetic)
- Invalid addresses or amounts
- Array length mismatches
- Division by zero
Deployment
Contracts can deploy other contracts:
use soroban_sdk::deploy::{Deployer, ContractIdPreimage};
let deployer = env.deployer();
let contract_id = deployer.deploy_wasm(&wasm_hash, &salt);
Common Patterns
State Management
Store contract state in Instance storage for contract-wide config:
const STATE_KEY: Symbol = symbol_short!("STATE");
pub fn init(env: Env, admin: Address) {
env.storage().instance().set(&STATE_KEY, &admin);
}
pub fn get_admin(env: Env) -> Address {
env.storage().instance().get(&STATE_KEY).unwrap()
}
Iterating Over Collections
Use iterator methods on Vec and Map:
let total: i128 = amounts
.iter()
.map(|x| x.unwrap())
.sum();
Cross-Contract Calls
Import contracts with contractimport! or create manual clients:
let other_contract = OtherContractClient::new(&env, &contract_address);
let result = other_contract.function(&args);
Project Setup
Requirements
- Rust toolchain v1.84.0 or higher (required for
wasm32v1-nonetarget) - Stellar CLI v25.1.0 or higher
- Install target:
rustup target add wasm32v1-none
Cargo.toml Configuration
Workspace-level Cargo.toml:
[workspace]
resolver = "2"
members = ["contracts/*"]
[workspace.dependencies]
soroban-sdk = "25"
[profile.release]
opt-level = "z"
overflow-checks = true
debug = 0
strip = "symbols"
debug-assertions = false
panic = "abort"
codegen-units = 1
lto = true
[profile.release-with-logs]
inherits = "release"
debug-assertions = true
Contract-level Cargo.toml:
[package]
name = "my-contract"
version = "0.0.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
doctest = false
[dependencies]
soroban-sdk = { workspace = true }
[dev-dependencies]
soroban-sdk = { workspace = true, features = ["testutils"] }
Building Contracts
Recommended: Use Stellar CLI (automatically sets correct target and profile):
stellar contract build
Equivalent manual command:
cargo build --target wasm32v1-none --release
Output: target/wasm32v1-none/release/contract_name.wasm
Optimize for production:
stellar contract optimize --wasm target/wasm32v1-none/release/contract_name.wasm
Produces: contract_name.optimized.wasm
Additional Resources
For detailed information on specific topics, see:
- Storage patterns and TTL management
- Authorization and auth context
- Testing strategies and utilities
- Token integration and Stellar Asset Contracts
- Common contract patterns and examples
- Security best practices and vulnerabilities ⚠️ CRITICAL
Official documentation: https://developers.stellar.org/docs/build/smart-contracts
Security Quick Reference
Critical rules to prevent vulnerabilities:
- ✅ Always authorize first: Call
require_auth()before any state changes - ✅ Validate all inputs: Check amounts, addresses, array lengths
- ✅ Prevent overflow: Use checked arithmetic for all math operations
- ✅ Initialize once: Use initialization flag to prevent reinitialization
- ✅ Extend TTL: Always extend TTL on persistent/instance storage writes
- ✅ Choose storage wisely: Persistent for critical data, Temporary for cache
- ✅ Test thoroughly: Cover authorization, overflows, edge cases
See references/security.md for complete security guidance.