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
⚠️ 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/from Val for storage
-
#[contracterror]
-
Defines error enums with repr(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 type
-
Map<K, V>
-
Ordered key-value dictionary
-
Bytes
-
Growable byte array
-
BytesN<N>
-
Fixed-size byte array
-
String
-
UTF-8 string type
-
U256 , I256
-
256-bit integers
Type Macros
-
vec![&env, item1, item2]
-
Create Vec
-
map![&env, (key1, val1), (key2, val2)]
-
Create Map
-
symbol_short!("text")
-
Create Symbol constant
-
bytes!(&env, 0x010203)
-
Create Bytes
-
bytesn!(&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(&param);
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-none target)
-
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.