Loading...
Loading...
Soroban smart contract development on Stellar (Rust SDK). Covers project setup, contract structure, storage types, authorization, cross-contract calls, events, error handling, testing (unit, integration, fuzz, property, mutation, fork, differential), security patterns and vulnerability classes, advanced architecture patterns (upgrades, factories, governance, DeFi primitives), and common pitfalls. Use when writing, testing, securing, or shipping Soroban contracts.
npx skill4agent add stellar/stellar-dev-skill soroban../assets/SKILL.md../dapp/SKILL.md../data/SKILL.md../zk-proofs/SKILL.md../standards/SKILL.mdas-soroban-sdk#![no_std]StringSymbolSymbolstellar contract init my-contract
cd my-contractmy-contract/
├── Cargo.toml
├── src/
│ └── lib.rs
└── contracts/
└── hello_world/
├── Cargo.toml
└── src/
└── lib.rs[package]
name = "my-contract"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
soroban-sdk = "25.0.1" # check https://crates.io/crates/soroban-sdk for latest
[dev-dependencies]
soroban-sdk = { version = "25.0.1", 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
[profile.release-with-logs]
inherits = "release"
debug-assertions = trueinitialize#![no_std]
use soroban_sdk::{contract, contractimpl, contracttype, Address, Env};
#[contracttype]
#[derive(Clone)]
pub enum DataKey {
Admin,
Value,
}
#[contract]
pub struct MyContract;
#[contractimpl]
impl MyContract {
// Runs once at deployment time.
pub fn __constructor(env: Env, admin: Address, initial_value: u32) {
env.storage().instance().set(&DataKey::Admin, &admin);
env.storage().instance().set(&DataKey::Value, &initial_value);
}
}stellar contract deploy \
--wasm target/wasm32-unknown-unknown/release/my_contract.wasm \
--source alice \
--network testnet \
-- \
--admin alice \
--initial_value 100__constructor()initialize#![no_std]
use soroban_sdk::{contract, contractimpl, symbol_short, vec, Env, Symbol, Vec};
#[contract]
pub struct HelloContract;
#[contractimpl]
impl HelloContract {
pub fn hello(env: Env, to: Symbol) -> Vec<Symbol> {
vec![&env, symbol_short!("Hello"), to]
}
}#![no_std]
use soroban_sdk::{contract, contractimpl, contracttype, Address, Env};
#[contracttype]
#[derive(Clone)]
pub enum DataKey {
Counter,
Admin,
UserBalance(Address),
}
#[contract]
pub struct CounterContract;
#[contractimpl]
impl CounterContract {
pub fn initialize(env: Env, admin: Address) {
if env.storage().instance().has(&DataKey::Admin) {
panic!("already initialized");
}
env.storage().instance().set(&DataKey::Admin, &admin);
env.storage().instance().set(&DataKey::Counter, &0u32);
}
pub fn increment(env: Env) -> u32 {
let mut count: u32 = env.storage().instance().get(&DataKey::Counter).unwrap_or(0);
count += 1;
env.storage().instance().set(&DataKey::Counter, &count);
// Extend TTL to prevent archival
env.storage().instance().extend_ttl(100, 518400); // threshold, ~30 days
count
}
pub fn get_count(env: Env) -> u32 {
env.storage().instance().get(&DataKey::Counter).unwrap_or(0)
}
}env.storage().instance().set(&key, &value);
env.storage().instance().get(&key);
env.storage().instance().extend_ttl(min_ttl, extend_to);env.storage().persistent().set(&key, &value);
env.storage().persistent().get(&key);
env.storage().persistent().extend_ttl(&key, min_ttl, extend_to);env.storage().temporary().set(&key, &value);
env.storage().temporary().get(&key);
env.storage().temporary().extend_ttl(&key, min_ttl, extend_to);// Check remaining TTL
let ttl = env.storage().persistent().get_ttl(&key);
// Extend if below threshold
const MIN_TTL: u32 = 17280; // ~1 day at 5s ledgers
const EXTEND_TO: u32 = 518400; // ~30 days
if ttl < MIN_TTL {
env.storage().persistent().extend_ttl(&key, MIN_TTL, EXTEND_TO);
}use soroban_sdk::{Address, Bytes, BytesN, Map, String, Symbol, Vec, I128, U256};
// Address - account or contract identifier
let addr: Address = env.current_contract_address();
// Symbol - short strings (max 32 chars)
let sym: Symbol = symbol_short!("transfer");
// String - longer strings
let s: String = String::from_str(&env, "Hello, Stellar!");
// Fixed-size bytes
let hash: BytesN<32> = env.crypto().sha256(&bytes);
// Collections
let v: Vec<u32> = vec![&env, 1, 2, 3];
let m: Map<Symbol, u32> = Map::new(&env);#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TokenMetadata {
pub name: String,
pub symbol: Symbol,
pub decimals: u32,
}
#[contracttype]
#[derive(Clone)]
pub enum DataKey {
Admin,
Balance(Address),
Allowance(Address, Address), // (owner, spender)
}#[contractimpl]
impl TokenContract {
pub fn transfer(env: Env, from: Address, to: Address, amount: i128) {
// Require 'from' to authorize this call
from.require_auth();
// Or require auth for specific arguments
from.require_auth_for_args((&to, amount).into_val(&env));
// Transfer logic...
}
}fn require_admin(env: &Env) {
let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap();
admin.require_auth();
}
pub fn set_admin(env: Env, new_admin: Address) {
require_admin(&env);
env.storage().instance().set(&DataKey::Admin, &new_admin);
}use soroban_sdk::{contract, contractimpl, Address, Env};
mod token_contract {
soroban_sdk::contractimport!(
file = "../token/target/wasm32-unknown-unknown/release/token.wasm"
);
}
#[contract]
pub struct VaultContract;
#[contractimpl]
impl VaultContract {
pub fn deposit(env: Env, user: Address, token: Address, amount: i128) {
user.require_auth();
// Create client for token contract
let token_client = token_contract::Client::new(&env, &token);
// Call transfer on token contract
token_client.transfer(&user, &env.current_contract_address(), &amount);
// Update vault state...
}
}use soroban_sdk::token::Client as TokenClient;
pub fn transfer_asset(env: Env, from: Address, to: Address, asset: Address, amount: i128) {
from.require_auth();
let token = TokenClient::new(&env, &asset);
token.transfer(&from, &to, &amount);
}use soroban_sdk::{contract, contractevent, contractimpl, Address, Env};
#[contractevent(topics = ["transfer"])]
pub struct TransferEvent {
pub from: Address,
pub to: Address,
pub amount: i128,
}
#[contract]
pub struct TokenContract;
#[contractimpl]
impl TokenContract {
pub fn transfer(env: Env, from: Address, to: Address, amount: i128) {
// ... transfer logic ...
// Emit event
TransferEvent { from, to, amount }.publish(&env);
}
}use soroban_sdk::contracterror;
#[contracterror]
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[repr(u32)]
pub enum ContractError {
AlreadyInitialized = 1,
NotInitialized = 2,
InsufficientBalance = 3,
Unauthorized = 4,
InvalidAmount = 5,
}
// Usage
pub fn transfer(env: Env, from: Address, to: Address, amount: i128) -> Result<(), ContractError> {
if amount <= 0 {
return Err(ContractError::InvalidAmount);
}
let balance: i128 = get_balance(&env, &from);
if balance < amount {
return Err(ContractError::InsufficientBalance);
}
// ... transfer logic ...
Ok(())
}# Build optimized WASM
stellar contract build
# Output: target/wasm32-unknown-unknown/release/my_contract.wasm# Generate and fund a new identity
stellar keys generate --global alice --network testnet --fund
# Deploy contract
stellar contract deploy \
--wasm target/wasm32-unknown-unknown/release/my_contract.wasm \
--source alice \
--network testnet
# Returns: CONTRACT_ID (starts with 'C')stellar contract invoke \
--id CONTRACT_ID \
--source alice \
--network testnet \
-- \
initialize \
--admin alicestellar contract invoke \
--id CONTRACT_ID \
--source alice \
--network testnet \
-- \
increment#![cfg(test)]
use super::*;
use soroban_sdk::testutils::Address as _;
use soroban_sdk::Env;
#[test]
fn test_increment() {
let env = Env::default();
let contract_id = env.register_contract(None, CounterContract);
let client = CounterContractClient::new(&env, &contract_id);
let admin = Address::generate(&env);
client.initialize(&admin);
assert_eq!(client.get_count(), 0);
assert_eq!(client.increment(), 1);
assert_eq!(client.increment(), 2);
assert_eq!(client.get_count(), 2);
}
#[test]
fn test_transfer_with_auth() {
let env = Env::default();
env.mock_all_auths(); // Auto-approve all auth requests
let contract_id = env.register_contract(None, TokenContract);
let client = TokenContractClient::new(&env, &contract_id);
let alice = Address::generate(&env);
let bob = Address::generate(&env);
// Mint tokens to alice
client.mint(&alice, &1000);
// Transfer from alice to bob
client.transfer(&alice, &bob, &100);
assert_eq!(client.balance(&alice), 900);
assert_eq!(client.balance(&bob), 100);
}symbol_short!()require_auth()stellar contract invoke --simAcceptedImplementedsoroban-sdkSee zk-proofs.md for Groth16 verification patterns, Poseidon usage, Noir/RISC Zero integration, and implementation guidance.
soroban-sdk#![cfg(test)]
use soroban_sdk::{testutils::Address as _, Address, Env};
// Import your contract
use crate::{Contract, ContractClient};
#[test]
fn test_basic_functionality() {
// Create test environment
let env = Env::default();
// Register contract
let contract_id = env.register_contract(None, Contract);
// Create typed client
let client = ContractClient::new(&env, &contract_id);
// Generate test addresses
let user = Address::generate(&env);
// Call contract functions
client.initialize(&user);
// Assert results
assert_eq!(client.get_value(), 0);
}#[test]
fn test_with_auth() {
let env = Env::default();
// Mock all authorizations automatically
env.mock_all_auths();
let contract_id = env.register_contract(None, TokenContract);
let client = TokenContractClient::new(&env, &contract_id);
let admin = Address::generate(&env);
let user1 = Address::generate(&env);
let user2 = Address::generate(&env);
// Initialize and mint
client.initialize(&admin);
client.mint(&user1, &1000);
// Transfer (requires auth from user1)
client.transfer(&user1, &user2, &100);
assert_eq!(client.balance(&user1), 900);
assert_eq!(client.balance(&user2), 100);
// Verify which auths were required
let auths = env.auths();
assert_eq!(auths.len(), 1);
// auths[0] contains (address, contract_id, function, args)
}#[test]
fn test_specific_auth() {
let env = Env::default();
let contract_id = env.register_contract(None, Contract);
let client = ContractClient::new(&env, &contract_id);
let user = Address::generate(&env);
// Mock auth only for specific address
env.mock_auths(&[MockAuth {
address: &user,
invoke: &MockAuthInvoke {
contract: &contract_id,
fn_name: "transfer",
args: (&user, &other, &100i128).into_val(&env),
sub_invokes: &[],
},
}]);
client.transfer(&user, &other, &100);
}#[test]
fn test_time_based() {
let env = Env::default();
let contract_id = env.register_contract(None, VestingContract);
let client = VestingContractClient::new(&env, &contract_id);
let beneficiary = Address::generate(&env);
// Set initial timestamp
env.ledger().set_timestamp(1000);
client.create_vesting(&beneficiary, &1000, &2000); // unlock at t=2000
// Try to claim before unlock
assert!(client.try_claim(&beneficiary).is_err());
// Advance time past unlock
env.ledger().set_timestamp(2500);
// Now claim succeeds
client.claim(&beneficiary);
}#[test]
fn test_ledger_manipulation() {
let env = Env::default();
// Set ledger sequence
env.ledger().set_sequence_number(1000);
// Set timestamp
env.ledger().set_timestamp(1704067200); // Jan 1, 2024
// Set network passphrase
env.ledger().set_network_id([0u8; 32]); // Custom network ID
// Get current values
let seq = env.ledger().sequence();
let ts = env.ledger().timestamp();
}#[test]
fn test_events() {
let env = Env::default();
let contract_id = env.register_contract(None, Contract);
let client = ContractClient::new(&env, &contract_id);
client.do_something();
// Get all events
let events = env.events().all();
// Check specific event
assert_eq!(events.len(), 1);
let event = &events[0];
// event.0 = contract_id
// event.1 = topics (Vec<Val>)
// event.2 = data (Val)
}#[test]
fn test_storage_ttl() {
let env = Env::default();
let contract_id = env.register_contract(None, Contract);
let client = ContractClient::new(&env, &contract_id);
client.store_data();
// Check TTL
let key = DataKey::MyData;
let ttl = env.as_contract(&contract_id, || {
env.storage().persistent().get_ttl(&key)
});
assert!(ttl > 0);
}#[test]
fn test_cross_contract() {
let env = Env::default();
// Register both contracts
let token_id = env.register_contract_wasm(None, token::WASM);
let vault_id = env.register_contract(None, VaultContract);
let token_client = token::Client::new(&env, &token_id);
let vault_client = VaultContractClient::new(&env, &vault_id);
env.mock_all_auths();
let user = Address::generate(&env);
// Setup: mint tokens to user
token_client.mint(&user, &1000);
// Test: deposit tokens into vault
vault_client.deposit(&user, &token_id, &500);
assert_eq!(token_client.balance(&user), 500);
assert_eq!(vault_client.balance(&user), 500);
}# Pull and run Stellar Quickstart
docker run --rm -it -p 8000:8000 \
--name stellar \
stellar/quickstart:latest \
--local \
--enable-soroban-rpc
# Or use Stellar CLI
stellar container start localimport * as StellarSdk from "@stellar/stellar-sdk";
const LOCAL_RPC = "http://localhost:8000/soroban/rpc";
const LOCAL_HORIZON = "http://localhost:8000";
const LOCAL_PASSPHRASE = "Standalone Network ; February 2017";
const rpc = new StellarSdk.rpc.Server(LOCAL_RPC);
const horizon = new StellarSdk.Horizon.Server(LOCAL_HORIZON);# Using Stellar CLI
stellar keys generate --global test-account --network local --fund
# Or via friendbot endpoint
curl "http://localhost:8000/friendbot?addr=G..."# Deploy contract to local network
stellar contract deploy \
--wasm target/wasm32-unknown-unknown/release/contract.wasm \
--source test-account \
--network local
# Invoke contract
stellar contract invoke \
--id CONTRACT_ID \
--source test-account \
--network local \
-- \
function_name \
--arg value# Testnet RPC: https://soroban-testnet.stellar.org
# Testnet Horizon: https://horizon-testnet.stellar.org
# Network Passphrase: "Test SDF Network ; September 2015"
# Friendbot: https://friendbot.stellar.org# Generate new identity
stellar keys generate --global my-testnet-key --network testnet
# Fund via Friendbot
stellar keys fund my-testnet-key --network testnet
# Or manually
curl "https://friendbot.stellar.org?addr=G..."# Deploy contract
stellar contract deploy \
--wasm target/wasm32-unknown-unknown/release/contract.wasm \
--source my-testnet-key \
--network testnet
# Install contract code (separate from deployment)
stellar contract install \
--wasm target/wasm32-unknown-unknown/release/contract.wasm \
--source my-testnet-key \
--network testnet// tests/integration/contract.test.ts
import * as StellarSdk from "@stellar/stellar-sdk";
const RPC_URL = process.env.RPC_URL || "http://localhost:8000/soroban/rpc";
const NETWORK_PASSPHRASE = process.env.NETWORK_PASSPHRASE || "Standalone Network ; February 2017";
describe("Contract Integration Tests", () => {
let rpc: StellarSdk.rpc.Server;
let keypair: StellarSdk.Keypair;
let contractId: string;
beforeAll(async () => {
rpc = new StellarSdk.rpc.Server(RPC_URL);
keypair = StellarSdk.Keypair.random();
// Fund account
await fundAccount(keypair.publicKey());
// Deploy contract
contractId = await deployContract(keypair);
});
test("should initialize contract", async () => {
const account = await rpc.getAccount(keypair.publicKey());
const contract = new StellarSdk.Contract(contractId);
const tx = new StellarSdk.TransactionBuilder(account, {
fee: "100",
networkPassphrase: NETWORK_PASSPHRASE,
})
.addOperation(
contract.call(
"initialize",
StellarSdk.Address.fromString(keypair.publicKey()).toScVal()
)
)
.setTimeout(30)
.build();
const simResult = await rpc.simulateTransaction(tx);
const preparedTx = StellarSdk.rpc.assembleTransaction(tx, simResult);
preparedTx.sign(keypair);
const result = await rpc.sendTransaction(preparedTx.build());
expect(result.status).not.toBe("ERROR");
});
});// tests/integration_test.rs
use soroban_sdk::{Env, Address};
use std::process::Command;
#[test]
#[ignore] // Run with: cargo test -- --ignored
fn integration_test_with_local_network() {
// Requires local network running
let output = Command::new("stellar")
.args([
"contract", "invoke",
"--id", "CONTRACT_ID",
"--source", "test-account",
"--network", "local",
"--",
"get_count"
])
.output()
.expect("Failed to invoke contract");
assert!(output.status.success());
}[dev-dependencies]
soroban-sdk = { version = "25.0.1", features = ["testutils"] } # match [dependencies] version
[profile.test]
opt-level = 0
debug = true# Run unit tests
cargo test
# Run with output
cargo test -- --nocapture
# Run specific test
cargo test test_transfer
# Run ignored (integration) tests
cargo test -- --ignoredname: Test Soroban Contract
on: [push, pull_request]
jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Add WASM target
run: rustup target add wasm32-unknown-unknown
- name: Run unit tests
run: cargo test
- name: Build contract
run: cargo build --release --target wasm32-unknown-unknown
integration-tests:
runs-on: ubuntu-latest
needs: unit-tests
services:
stellar:
image: stellar/quickstart:latest
ports:
- 8000:8000
options: >-
--health-cmd "curl -f http://localhost:8000 || exit 1"
--health-interval 10s
--health-timeout 5s
--health-retries 10
steps:
- uses: actions/checkout@v4
- name: Install Stellar CLI
run: |
cargo install stellar-cli --locked
- name: Deploy and test
run: |
stellar keys generate --global ci-test --network local --fund
stellar contract deploy \
--wasm target/wasm32-unknown-unknown/release/contract.wasm \
--source ci-test \
--network localproject/
├── src/
│ └── lib.rs
├── tests/
│ ├── common/
│ │ └── mod.rs # Shared test utilities
│ ├── unit/
│ │ ├── mod.rs
│ │ └── transfer.rs
│ └── integration/
│ └── full_flow.rs
└── Cargo.toml// tests/common/mod.rs
use soroban_sdk::{testutils::Address as _, Address, Env};
use crate::{Contract, ContractClient};
pub fn setup_contract(env: &Env) -> (Address, ContractClient) {
let contract_id = env.register_contract(None, Contract);
let client = ContractClient::new(env, &contract_id);
let admin = Address::generate(env);
env.mock_all_auths();
client.initialize(&admin);
(contract_id, client)
}
pub fn create_funded_user(env: &Env, client: &ContractClient, amount: i128) -> Address {
let user = Address::generate(env);
client.mint(&user, &amount);
user
}cargo-fuzzSorobanArbitrary#[contracttype]SorobanArbitrary"testutils"# Install nightly Rust + cargo-fuzz
rustup install nightly
cargo install --locked cargo-fuzz
# Initialize fuzz targets
cargo fuzz initCargo.toml[lib]
crate-type = ["lib", "cdylib"]fuzz/Cargo.toml[dependencies]
soroban-sdk = { version = "25.0.1", features = ["testutils"] }// fuzz/fuzz_targets/fuzz_deposit.rs
#![no_main]
use libfuzzer_sys::fuzz_target;
use soroban_sdk::{testutils::Address as _, Address, Env};
use my_contract::{Contract, ContractClient};
fuzz_target!(|input: (u64, i128)| {
let (seed, amount) = input;
let env = Env::default();
env.mock_all_auths();
let contract_id = env.register(Contract, ());
let client = ContractClient::new(&env, &contract_id);
let user = Address::generate(&env);
// Initialize
client.initialize(&user);
// Fuzz deposit — should never panic unexpectedly
let _ = client.try_deposit(&user, &amount);
});# Run (use --sanitizer=thread on macOS)
cargo +nightly fuzz run fuzz_deposit
# Generate code coverage
cargo +nightly fuzz coverage fuzz_depositproptestSorobanArbitrarycargo test#[cfg(test)]
mod prop_tests {
use super::*;
use proptest::prelude::*;
use soroban_sdk::{testutils::Address as _, Env};
proptest! {
#[test]
fn deposit_then_withdraw_preserves_balance(amount in 1i128..=i128::MAX) {
let env = Env::default();
env.mock_all_auths();
let contract_id = env.register(Contract, ());
let client = ContractClient::new(&env, &contract_id);
let user = Address::generate(&env);
client.initialize(&user);
client.deposit(&user, &amount);
client.withdraw(&user, &amount);
prop_assert_eq!(client.balance(&user), 0);
}
}
}cargo-fuzzproptesttest_snapshots/// Fetch deployed contract for comparison
// $ stellar contract fetch --id C... --out-file deployed.wasm
mod deployed {
soroban_sdk::contractimport!(file = "deployed.wasm");
}
#[test]
fn test_upgrade_compatibility() {
let env = Env::default();
env.mock_all_auths();
// Register both versions
let old_id = env.register_contract_wasm(None, deployed::WASM);
let new_id = env.register(NewContract, ());
let old_client = deployed::Client::new(&env, &old_id);
let new_client = NewContractClient::new(&env, &new_id);
let user = Address::generate(&env);
// Run identical operations and compare
old_client.initialize(&user);
new_client.initialize(&user);
assert_eq!(old_client.get_value(), new_client.get_value());
}# Create snapshot of deployed contract
stellar snapshot create --address C... --output json --out snapshot.json
# Optionally at a specific ledger
stellar snapshot create --address C... --ledger 12345678 --output json --out snapshot.json#[test]
fn test_against_mainnet_state() {
let env = Env::from_ledger_snapshot_file("snapshot.json");
env.mock_all_auths();
let contract_id = /* contract address from snapshot */;
let client = ContractClient::new(&env, &contract_id);
// Test operations against real state
let result = client.get_value();
assert!(result > 0);
}cargo-mutantscargo install --locked cargo-mutants
cargo mutantsmutants.out/diff/# Simulate contract invocation to see resource costs
stellar contract invoke \
--id CONTRACT_ID \
--source alice \
--network testnet \
--sim-only \
-- \
function_name --arg valueimport { StellarPlus } from 'stellar-plus';
const profilerPlugin = new StellarPlus.Utils.Plugins.sorobanTransaction.profiler();
// Collects CPU instructions, RAM, ledger reads/writes
// Aggregation: sum, average, standard deviation
// Output: CSV, formatted text tablesdelegatecallrequire_auth()// BAD: No authorization check
pub fn withdraw(env: Env, to: Address, amount: i128) {
transfer_tokens(&env, &to, amount);
}// GOOD: Requires authorization from admin
pub fn withdraw(env: Env, to: Address, amount: i128) {
let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap();
admin.require_auth();
transfer_tokens(&env, &to, amount);
}require_auth()require_auth_for_args// BAD: Can be called multiple times
pub fn initialize(env: Env, admin: Address) {
env.storage().instance().set(&DataKey::Admin, &admin);
}// GOOD: Prevents reinitialization
pub fn initialize(env: Env, admin: Address) {
if env.storage().instance().has(&DataKey::Initialized) {
panic!("already initialized");
}
env.storage().instance().set(&DataKey::Admin, &admin);
env.storage().instance().set(&DataKey::Initialized, &true);
}
// Alternative: Check for admin existence
pub fn initialize(env: Env, admin: Address) {
if env.storage().instance().has(&DataKey::Admin) {
panic!("already initialized");
}
env.storage().instance().set(&DataKey::Admin, &admin);
}// BAD: Calls any contract passed as parameter
pub fn swap(env: Env, token: Address, amount: i128) {
let client = token::Client::new(&env, &token);
client.transfer(...); // Could be malicious contract
}// GOOD: Validate against known allowlist
pub fn swap(env: Env, token: Address, amount: i128) {
let allowed_tokens: Vec<Address> = env.storage()
.instance()
.get(&DataKey::AllowedTokens)
.unwrap();
if !allowed_tokens.contains(&token) {
panic!("token not allowed");
}
let client = token::Client::new(&env, &token);
client.transfer(...);
}
// Or validate against Stellar Asset Contract
pub fn swap_sac(env: Env, asset: Address, amount: i128) {
// SACs have known, predictable addresses
// Verify it's a legitimate SAC if needed
}// BAD: Unchecked arithmetic
pub fn deposit(env: Env, user: Address, amount: i128) {
let balance: i128 = get_balance(&env, &user);
set_balance(&env, &user, balance + amount); // Can overflow
}// GOOD: Use checked arithmetic
pub fn deposit(env: Env, user: Address, amount: i128) {
let balance: i128 = get_balance(&env, &user);
let new_balance = balance.checked_add(amount)
.expect("overflow");
set_balance(&env, &user, new_balance);
}
// Also validate inputs
pub fn deposit(env: Env, user: Address, amount: i128) {
if amount <= 0 {
panic!("invalid amount");
}
// ... rest of logic
}// BAD: Same prefix for different data
env.storage().persistent().set(&symbol_short!("data"), &user_balance);
env.storage().persistent().set(&symbol_short!("data"), &config); // Overwrites!// GOOD: Use typed enum for keys
#[contracttype]
#[derive(Clone)]
pub enum DataKey {
Admin,
Balance(Address),
Config,
Allowance(Address, Address),
}
env.storage().persistent().set(&DataKey::Balance(user), &balance);
env.storage().instance().set(&DataKey::Config, &config);// Use atomic operations where possible
pub fn swap(env: Env, user: Address, amount_in: i128, min_out: i128) {
user.require_auth();
// Perform all checks and state changes atomically
let balance = get_balance(&env, &user);
if balance < amount_in {
panic!("insufficient balance");
}
let amount_out = calculate_output(amount_in);
if amount_out < min_out {
panic!("slippage exceeded");
}
// Update all state together
set_balance(&env, &user, balance - amount_in);
transfer_output(&env, &user, amount_out);
}// Extend TTL for critical data
pub fn critical_operation(env: Env) {
// Always extend instance storage
env.storage().instance().extend_ttl(
100, // threshold
518400, // extend_to (~30 days)
);
// Extend specific persistent keys
env.storage().persistent().extend_ttl(
&DataKey::CriticalData,
100,
518400,
);
}
// Consider restoration costs in design
// Archived data can be restored, but requires transaction// Validate all external data
pub fn process_oracle_price(env: Env, oracle: Address, asset: Address) -> i128 {
// Validate oracle is trusted
let trusted_oracles: Vec<Address> = env.storage()
.instance()
.get(&DataKey::TrustedOracles)
.unwrap();
if !trusted_oracles.contains(&oracle) {
panic!("untrusted oracle");
}
let price: i128 = oracle_client.get_price(&asset);
// Sanity check the value
if price <= 0 || price > MAX_REASONABLE_PRICE {
panic!("invalid price");
}
price
}// Check if clawback is enabled
const issuerAccount = await server.loadAccount(asset.issuer);
const clawbackEnabled = issuerAccount.flags.auth_clawback_enabled;
if (clawbackEnabled) {
// Warn user or reject asset
}| Firm | Specialty |
|---|---|
| OtterSec | Smart contract audits |
| Veridise | Tool-assisted audits, security checklist |
| Runtime Verification | Formal methods, Komet tool |
| CoinFabrik | Static analysis (Scout), manual audits |
| QuarksLab | Security research |
| Coinspect | Security audits |
| Certora | Formal verification (Sunbeam Prover) |
| Halborn | Security assessments |
| Zellic | Blockchain + cryptography research |
| Code4rena | Competitive audit platform |
cargo install cargo-scout-auditcargo scout-auditoverflow-checkunprotected-update-current-contract-wasmset-contract-storageunrestricted-transfer-fromdivide-before-multiplydos-unbounded-operationunsafe-unwrapsdkdetectorssoroban-scannerauth_missingunchecked_ft_transfercvlr_assert!cvlr_assume!cvlr_satisfy!upgradecontractmeta!#![no_std]
use soroban_sdk::{contract, contractimpl, contractmeta, contracttype, Address, BytesN, Env};
contractmeta!(key = "binver", val = "1.0.0");
#[contracttype]
#[derive(Clone)]
pub enum DataKey {
Admin,
AppVersion,
}
#[contract]
pub struct Upgradeable;
#[contractimpl]
impl Upgradeable {
pub fn __constructor(env: Env, admin: Address) {
env.storage().instance().set(&DataKey::Admin, &admin);
env.storage().instance().set(&DataKey::AppVersion, &1u32);
}
pub fn upgrade(env: Env, new_wasm_hash: BytesN<32>) {
let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap();
admin.require_auth();
env.deployer().update_current_contract_wasm(new_wasm_hash);
}
}migratenew_version > current_version#![no_std]
use soroban_sdk::{contract, contractimpl, Address, BytesN, Env, Val, Vec};
#[contract]
pub struct Factory;
#[contractimpl]
impl Factory {
pub fn deploy(
env: Env,
owner: Address,
wasm_hash: BytesN<32>,
salt: BytesN<32>,
constructor_args: Vec<Val>,
) -> Address {
owner.require_auth();
env.deployer()
.with_address(env.current_contract_address(), salt)
.deploy_v2(wasm_hash, constructor_args)
}
}propose_*execute_*cancel_*total_assetstotal_sharesinstancepersistenttemporaryopt-level = "z"lto = truepanic = "abort"Error: contract exceeds maximum size# Cargo.toml - Use aggressive optimization
[profile.release]
opt-level = "z" # Optimize for size
lto = true # Link-time optimization
codegen-units = 1 # Single codegen unit
panic = "abort" # Smaller panic handling
strip = "symbols" # Remove symbolssymbol_short!()cargo bloat# Check contract size
ls -la target/wasm32-unknown-unknown/release/*.wasm
# Analyze what's taking space
cargo install cargo-bloat
cargo bloat --release --target wasm32-unknown-unknown#![no_std]error: cannot find macro `println` in this scope
error[E0433]: failed to resolve: use of undeclared crate or module `std`// MUST be first line of lib.rs
#![no_std]
use soroban_sdk::{contract, contractimpl, Env};
// Use soroban_sdk equivalents instead of std:
// - soroban_sdk::String instead of std::string::String
// - soroban_sdk::Vec instead of std::vec::Vec
// - soroban_sdk::Map instead of std::collections::HashMap// Proactively extend TTL in operations that use data
pub fn use_data(env: Env) {
// Extend instance storage
env.storage().instance().extend_ttl(
50, // If TTL < 50, extend
518400, // Extend to ~30 days
);
// Extend specific persistent keys
env.storage().persistent().extend_ttl(
&DataKey::ImportantData,
50,
518400,
);
// Now use the data...
}See Part 1: Contract Development above for full TTL management patterns and storage type guidance.
// Instance: Shared config, survives with contract
env.storage().instance().set(&DataKey::Admin, &admin);
// Persistent: User data, can be archived but restored
env.storage().persistent().set(&DataKey::Balance(user), &balance);
// Temporary: Truly temporary, auto-deleted, cheapest
env.storage().temporary().set(&DataKey::Cache(key), &value);require_auth()#[test]
fn test_auth() {
let env = Env::default();
// DON'T just mock all auths blindly
// env.mock_all_auths(); // Be careful with this!
// DO test specific auth requirements with mock_auths()
env.mock_auths(&[MockAuth {
address: &user,
invoke: &MockAuthInvoke {
contract: &contract_id,
fn_name: "transfer",
args: (&user, &other, &100i128).into_val(&env),
sub_invokes: &[],
},
}]);
client.transfer(&user, &other, &100);
assert!(!env.auths().is_empty());
}See Part 2: Testing Strategy above for comprehensive auth testing patterns including, specific auth mocking, and cross-contract auth.mock_all_auths()
Error: tx_bad_authimport * as StellarSdk from "@stellar/stellar-sdk";
// ALWAYS use correct passphrase for network
const PASSPHRASES = {
mainnet: StellarSdk.Networks.PUBLIC,
// "Public Global Stellar Network ; September 2015"
testnet: StellarSdk.Networks.TESTNET,
// "Test SDF Network ; September 2015"
local: "Standalone Network ; February 2017",
};
// When building transactions
const tx = new StellarSdk.TransactionBuilder(account, {
fee: StellarSdk.BASE_FEE,
networkPassphrase: PASSPHRASES.testnet, // Match your network!
});Error: Account not found
Status: 404// Testnet - use Friendbot
await fetch(`https://friendbot.stellar.org?addr=${publicKey}`);
// Mainnet - must receive XLM from existing account
const tx = new StellarSdk.TransactionBuilder(funderAccount, {
fee: StellarSdk.BASE_FEE,
networkPassphrase: StellarSdk.Networks.PUBLIC,
})
.addOperation(
StellarSdk.Operation.createAccount({
destination: newAccountPublicKey,
startingBalance: "2", // Minimum ~1 XLM for base reserve
})
)
.setTimeout(180)
.build();Error: op_no_trust// Check if trustline exists
const account = await server.loadAccount(destination);
const hasTrustline = account.balances.some(
(b) =>
b.asset_type !== "native" &&
b.asset_code === asset.code &&
b.asset_issuer === asset.issuer
);
if (!hasTrustline) {
// Create trustline first
const trustTx = new StellarSdk.TransactionBuilder(destAccount, {
fee: StellarSdk.BASE_FEE,
networkPassphrase,
})
.addOperation(StellarSdk.Operation.changeTrust({ asset }))
.setTimeout(180)
.build();
// Sign and submit...
}Error: tx_bad_seq// Cause 1: Stale account data
// Solution: Always load fresh account before building tx
const account = await server.loadAccount(publicKey);
// Cause 2: Parallel transactions
// Solution: Use sequence number management
class SequenceManager {
private sequence: bigint;
async getNext(server: Horizon.Server, publicKey: string) {
if (!this.sequence) {
const account = await server.loadAccount(publicKey);
this.sequence = BigInt(account.sequence);
}
this.sequence++;
return this.sequence.toString();
}
}
// Cause 3: Transaction timeout without resubmit
// Solution: Rebuild with fresh sequence on timeoutError: transaction simulation failed
Error: insufficient resources// ALWAYS simulate before submitting Soroban transactions
const simulation = await rpc.simulateTransaction(transaction);
if (StellarSdk.rpc.Api.isSimulationError(simulation)) {
throw new Error(`Simulation failed: ${simulation.error}`);
}
// Use assembleTransaction to add correct resources
const preparedTx = StellarSdk.rpc.assembleTransaction(
transaction,
simulation
).build();
// Now sign and submit preparedTx, not original transactionisConnected()import { isConnected, isAllowed } from "@stellar/freighter-api";
async function checkFreighter() {
// Check if extension is installed
const connected = await isConnected();
if (!connected) {
// Prompt user to install
window.open("https://freighter.app", "_blank");
return;
}
// Check if app is allowed
const allowed = await isAllowed();
if (!allowed) {
// Need to request permission
await setAllowed();
}
}import { getNetwork } from "@stellar/freighter-api";
async function validateNetwork() {
const walletNetwork = await getNetwork();
const appNetwork = process.env.NEXT_PUBLIC_STELLAR_NETWORK;
if (walletNetwork !== appNetwork) {
throw new Error(
`Please switch Freighter to ${appNetwork}. Currently on ${walletNetwork}`
);
}
}Error: tx_too_late// Set appropriate timeout based on expected confirmation time
const tx = new StellarSdk.TransactionBuilder(account, {
fee: StellarSdk.BASE_FEE,
networkPassphrase,
})
.addOperation(/* ... */)
.setTimeout(180) // 3 minutes - adjust as needed
.build();
// Handle timeout gracefully
async function submitWithRetry(signedXdr: string) {
try {
return await submitTransaction(signedXdr);
} catch (error) {
if (error.response?.data?.extras?.result_codes?.transaction === "tx_too_late") {
// Rebuild with fresh blockhash and retry
const newTx = await rebuildTransaction(signedXdr);
return await submitTransaction(newTx);
}
throw error;
}
}Error: identity "alice" not found# List existing identities
stellar keys list
# Generate new identity
stellar keys generate --global alice
# For testnet with funding
stellar keys generate --global alice --network testnet --fund
# Specify identity location
stellar keys generate alice --config-dir /custom/pathError: invalid argument format# Use correct argument syntax
# Addresses: just the G... or C... string
stellar contract invoke \
--id CONTRACT_ID \
--source alice \
--network testnet \
-- \
transfer \
--from GABC... \
--to GDEF... \
--amount 1000
# Complex types: use JSON
stellar contract invoke \
--id CONTRACT_ID \
-- \
complex_fn \
--data '{"field1": "value", "field2": 123}'| Code | Meaning | Common Fix |
|---|---|---|
| Signature invalid | Check network passphrase |
| Wrong sequence | Reload account |
| Transaction expired | Rebuild and resubmit |
| Missing trustline | Create trustline first |
| Insufficient balance | Add funds |
| Below minimum balance | Maintain reserve |