๐Ÿ› ๏ธ Building Smart Contracts on Stellar SOROBAN

Write, test, deploy, and invoke smart contracts using Rust & WebAssembly on the Stellar Soroban platform

Development Workflow

โœ๏ธ
Write
Rust โ†’ WASM
โ†’
๐Ÿงช
Test
Local & Testnet
โ†’
๐Ÿš€
Deploy
Mainnet
โ†’
โšก
Invoke
Interact
Your Progress
0 / 4
๐ŸŒŸ

Getting Started

Follow the official learning path from zero to deployed dApp
โ–ผ
๐Ÿ’ก
Start with Scaffold Stellar for the fastest path to a working dApp on Stellar. It bundles a CLI, contract templates, a smart contract registry, and a modern frontend for building full-stack dApps. Use this guide for a lower-level tour of smart contract development from the main Stellar CLI.
๐Ÿ› ๏ธ
Setup

Install Rust, configure your editor, and set up the Stellar CLI. The entire toolchain is free and runs on Windows, macOS, and Linux.

Terminal โ€” install toolchain
# 1. Install Rust (if not already installed)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# On Windows: download and run rustup-init.exe from https://rustup.rs

# 2. Add the WebAssembly compilation target
rustup target add wasm32-unknown-unknown

# 3. Install the Stellar CLI
cargo install --locked stellar-cli

# 4. Verify everything works
rustc --version          # โ†’ rustc 1.XX.0
stellar --version        # โ†’ stellar-cli 21.X.X
cargo build --version    # โ†’ cargo 1.XX.0

Editor setup โ€” install rust-analyzer for VS Code. It provides inline type hints, auto-complete for SDK types, and red-underline errors before you compile.

  • Rust installed โ€” rustc --version prints 1.70+
  • WASM target added โ€” rustup target list --installed shows wasm32-unknown-unknown
  • Stellar CLI installed โ€” stellar --version prints 21.x+
  • Editor configured โ€” rust-analyzer running in VS Code / IntelliJ
โš ๏ธ
Windows users: If you don't have a C/C++ linker, install Visual Studio Build Tools with the "Desktop development with C++" workload. Rust needs a linker to produce native test binaries.
1
๐Ÿ‘‹ Hello World

Create your first smart contract on Stellar. Learn how to write, test, and build your contract using Rust and the Stellar CLI. Covers project structure, #[contract], #[contractimpl], and unit tests.

Terminal โ€” create project
# Scaffold a new Soroban project
stellar contract init hello-world
cd hello-world

# Project structure created:
# hello-world/
# โ”œโ”€โ”€ Cargo.toml
# โ”œโ”€โ”€ src/
# โ”‚   โ””โ”€โ”€ lib.rs       โ† your contract code
# โ””โ”€โ”€ test/
#     โ””โ”€โ”€ test.rs      โ† unit tests
Rust โ€” src/lib.rs
#![no_std]
use soroban_sdk::{contract, contractimpl, symbol_short, Env, Symbol};

#[contract]                      // marks the struct as a contract
pub struct HelloContract;

#[contractimpl]                  // generates the WASM entry points
impl HelloContract {
    pub fn hello(env: Env, to: Symbol) -> Vec<Symbol> {
        soroban_sdk::vec![&env,
            symbol_short!("Hello"),
            to,
        ]
    }
}
Terminal โ€” build & test
# Run unit tests
cargo test

# Build for WASM deployment
cargo build --target wasm32-unknown-unknown --release

# Or with the OLIGHFT CLI:
olighft build
olighft test
๐Ÿ’ก
Key concepts: #[contract] marks the struct as a Soroban contract. #[contractimpl] auto-generates WASM entry points for every pub fn. The Env parameter is the gateway to storage, auth, crypto, and cross-contract calls โ€” it's always the first argument.
  • Project created โ€” stellar contract init succeeded
  • Tests pass โ€” cargo test shows green
  • WASM built โ€” .wasm file exists in target/wasm32-.../release/
2
๐Ÿš€ Deploy to Testnet

Deploy your compiled contract to Stellar's Testnet using the CLI. Interact with other contracts, test functionality with free testnet XLM from Friendbot, debug issues, and prepare for Mainnet deployment.

Terminal โ€” configure identity & network
# 1. Generate a new identity (creates a keypair locally)
stellar keys generate alice --network testnet

# 2. View the public address
stellar keys address alice
# โ†’ GBXYZ...

# 3. Fund with Friendbot (free testnet XLM)
stellar keys fund alice --network testnet
# โ†’ Account GBXYZ... funded with 10,000 XLM on testnet

# 4. Verify the balance
stellar account show --network testnet --source alice
Terminal โ€” deploy contract
# Deploy the WASM to testnet
stellar contract deploy \
  --wasm target/wasm32-unknown-unknown/release/hello_world.wasm \
  --network testnet \
  --source alice
# โ†’ Contract ID: CABC123...DEF

# Invoke the contract
stellar contract invoke \
  --id CABC123...DEF \
  --network testnet \
  --source alice \
  -- hello --to World
# โ†’ ["Hello", "World"]

# Or with the OLIGHFT CLI (one step):
olighft deploy -n testnet -s alice
๐ŸŒ
Networks: testnet is free โ€” use it for development and testing. mainnet uses real XLM. You can also run a local sandbox with olighft sandbox for faster iteration without network latency.
  • Identity created โ€” stellar keys address alice shows a public key
  • Account funded โ€” Friendbot credited 10,000 testnet XLM
  • Contract deployed โ€” received a Contract ID (C...)
  • Invoked successfully โ€” stellar contract invoke returned expected output
3
๐Ÿ’พ Storing Data

Follow the increment example to write a contract that stores and retrieves data on the Stellar network. Learn about the three storage types (instance, persistent, temporary) and TTL management.

Rust โ€” increment contract
#![no_std]
use soroban_sdk::{contract, contractimpl, contracttype, Env};

// Storage keys are enums โ€” type-safe and collision-free
#[contracttype]
pub enum DataKey {
    Counter,
}

#[contract]
pub struct IncrementContract;

#[contractimpl]
impl IncrementContract {
    /// Increment and return the counter
    pub fn increment(env: Env) -> u32 {
        // Read from instance storage (default 0 if absent)
        let count: u32 = env.storage().instance()
            .get(&DataKey::Counter)
            .unwrap_or(0);

        let new_count = count + 1;

        // Write back to instance storage
        env.storage().instance()
            .set(&DataKey::Counter, &new_count);

        new_count
    }

    /// Read the current count without modifying it
    pub fn get_count(env: Env) -> u32 {
        env.storage().instance()
            .get(&DataKey::Counter)
            .unwrap_or(0)
    }
}
Tier Lifetime Use For API
Instance Same as contract Admin, config, totals env.storage().instance()
Persistent Survives archival (needs TTL) Balances, positions, profiles env.storage().persistent()
Temporary Auto-expires Nonces, caches, rate limiters env.storage().temporary()
โš ๏ธ
TTL management: Persistent data can be archived (not deleted) if its TTL expires. Call extend_ttl(key, threshold, extend_to) in frequently-used functions to keep data alive. Instance storage TTL is managed automatically with the contract's own TTL.
  • Understand DataKey enums โ€” type-safe storage keys
  • Write to storage โ€” .set() persists data to the ledger
  • Read from storage โ€” .get() retrieves or returns None
  • Know the 3 tiers โ€” instance vs persistent vs temporary
4
๐Ÿ“ก Deploy the Increment Contract

Deploy the increment smart contract on Testnet using the Stellar CLI. Practice the full cycle: build โ†’ optimize โ†’ install WASM โ†’ deploy instance โ†’ invoke โ†’ verify state.

Terminal โ€” full deploy cycle
# 1. Build the WASM
cargo build --target wasm32-unknown-unknown --release

# 2. (Optional) Optimize the binary size
stellar contract optimize \
  --wasm target/wasm32-unknown-unknown/release/increment.wasm
# โ†’ optimized to 1.2 KiB

# 3. Install the WASM bytecode on-chain (returns a hash)
stellar contract install \
  --wasm target/wasm32-unknown-unknown/release/increment.wasm \
  --network testnet --source alice
# โ†’ WASM hash: abc123...def

# 4. Deploy an instance from the installed bytecode
stellar contract deploy \
  --wasm-hash abc123...def \
  --network testnet --source alice
# โ†’ Contract ID: CXYZ789...

# 5. Invoke the increment function
stellar contract invoke \
  --id CXYZ789... \
  --network testnet --source alice \
  -- increment
# โ†’ 1

# 6. Invoke again โ€” state persists!
stellar contract invoke \
  --id CXYZ789... \
  --network testnet --source alice \
  -- increment
# โ†’ 2

# 7. Read without modifying
stellar contract invoke \
  --id CXYZ789... \
  --network testnet --source alice \
  -- get_count
# โ†’ 2
๐Ÿ’ก
Install vs Deploy: install uploads the Wasm bytecode to the ledger (returns a hash). deploy creates a new contract instance from that hash. Multiple instances can share the same bytecode โ€” update once, and all share the upgrade. This is how OLIGHFT deploys 6 card tier contracts from one token WASM.
Terminal โ€” OLIGHFT CLI shortcut
# Build + optimise + deploy in one step
olighft deploy -c token -n testnet -s alice

# Deploy all contracts in dependency order
olighft deploy -n testnet -s alice

# Dry-run to preview commands before executing
olighft deploy -n testnet -s alice --dry-run
  • Build โ†’ WASM โ€” compiled with --release
  • Optimize โ€” binary stripped to minimal size
  • Install bytecode โ€” WASM hash received
  • Deploy instance โ€” Contract ID received
  • Invoke & verify โ€” state changes persist across calls
5
๐ŸŒ Build a Hello World Frontend

Build a web frontend for the Hello World contract using the Stellar JavaScript SDK. Connect a wallet, send transactions, and display contract results in a modern React/Next.js app.

Terminal โ€” generate JS bindings
# Generate TypeScript bindings from a deployed contract
stellar contract bindings typescript \
  --network testnet \
  --id CABC123...DEF \
  --output-dir ./src/contracts/hello-world

# This creates a fully-typed client:
# ./src/contracts/hello-world/
# โ”œโ”€โ”€ index.ts       โ† auto-generated client
# โ”œโ”€โ”€ types.ts       โ† contract types (enums, structs)
# โ””โ”€โ”€ package.json
TypeScript โ€” invoke from frontend
import { Client } from './contracts/hello-world';

// Initialize with RPC URL + contract ID
const client = new Client({
  contractId: 'CABC123...DEF',
  networkPassphrase: 'Test SDF Network ; September 2015',
  rpcUrl: 'https://soroban-testnet.stellar.org',
});

// Call a contract function โ€” fully typed!
const result = await client.hello({ to: 'World' });
console.log(result); // โ†’ ["Hello", "World"]

// For write operations, sign with a wallet (Freighter, etc.)
const tx = await client.increment();
const signed = await wallet.signTransaction(tx.toXDR());
const res = await client.submit(signed);
๐Ÿ”—
Wallet integration: Use Freighter (browser extension) for signing transactions. The JS SDK's signTransaction() method pops up Freighter for user approval โ€” no private keys in your frontend code.
  • Bindings generated โ€” TypeScript client auto-created from contract ABI
  • Read call working โ€” client.hello() returns data from contract
  • Write call working โ€” transaction signed via wallet, state updated on-chain
  • UI rendering โ€” contract results displayed in the frontend
๐Ÿ—๏ธ
Scaffold Stellar is the fastest path for full-stack dApps โ€” it bundles contract templates, a smart contract registry, and a modern frontend toolkit. View on GitHub โ†—
๐Ÿ“

Core Concepts

Fundamental building blocks of Stellar smart contracts
โ–ผ

โš™๏ธ What is Soroban?

Soroban is Stellar's smart contract platform โ€” small programs written in Rust and compiled to WebAssembly (Wasm) for deployment on the Stellar network. It offers a programmable, high-performance environment for decentralized applications (dApps) with high-speed execution, predictable fees, and resource limits that keep the network efficient.

๐Ÿ“
Soroban is not a new blockchain โ€” it runs directly on the existing Stellar network. Other languages may be supported in the future, but currently only Rust is supported for writing contracts.

๐Ÿฆ€ Contract Rust Dialect

Stellar smart contracts have resource limits, security considerations, and constraints that force contracts to use only a narrow subset of the full Rust language. Contracts must use specialized libraries for most tasks:

  • The Rust standard library (std) is not available โ€” all contracts use #![no_std]
  • Most third-party crates will not work off-the-shelf due to these constraints
  • Some crates can be adapted for use in contracts, and others may be incorporated into the host environment as host objects or functions
  • No heap allocator โ€” use SDK types like Vec, Map, String which use the Soroban environment's memory
  • No floats โ€” floating point math is not supported; use integer arithmetic with basis points

๐Ÿงฐ Soroban Rust SDK

Contracts are developed using the Soroban Rust SDK, which consists of a Rust crate and a CLI tool:

๐Ÿ“ฆ SDK Crate โ€” olighft-sdk

The SDK crate is a #![no_std] library (rlib) that re-exports every Soroban primitive and layers project-specific helpers on top. Import it once and you never need to touch soroban-sdk directly:

Rust โ€” using the SDK
#![no_std]
use olighft_sdk::*;  // Env, Address, Vec, Map, contract, contractimpl, โ€ฆ
                      // plus types, math, crypto, auth, storage, invoke
Cargo.toml โ€” add as dependency
[dependencies]
olighft-sdk = { path = "../sdk" }

The crate is organised into six modules. Every public item is re-exported from the root, so use olighft_sdk::* is all you need:

Module Purpose Key Exports
types Project-wide constants LEDGERS_PER_DAY, BPS_SCALE, RATE_SCALE, MAX_BATCH, MAX_GENERATIONS
math Checked arithmetic & basis-point helpers apply_bps(), fee_bps(), convert_rate(), isqrt(), price_impact_bps(), daily_reward()
crypto Cryptographic hashing & verification sha256(), keccak256(), verify_ed25519(), hash_bytes(), hash_pair()
auth Authorisation wrappers require_auth(), require_admin()
storage Typed get/set/has/remove across all 3 Soroban tiers instance_get(), persistent_set(), temporary_has(), instance_get_or(), โ€ฆ
invoke Cross-contract invocation via stable IDs token_client(), transfer(), invoke(), ContractId, register_contract(), resolve_contract()

๐Ÿ”ข math โ€” Checked Arithmetic & Basis Points

All math uses i128 checked operations โ€” overflow returns None instead of panicking. Basis-point scale (รท10 000) is the standard throughout all contracts.

Rust โ€” math module
use olighft_sdk::{apply_bps, fee_bps, isqrt, daily_reward, convert_rate};

// 0.3 % swap fee on 10 000 tokens
let fee = fee_bps(10_000, 30);        // โ†’ Some(30)

// Boost multiplier: 1.5ร— (15 000 bps)
let boosted = apply_bps(amount, 15_000); // โ†’ Some(amount * 1.5)

// AMM liquidity: integer sqrt for LP shares
let shares = isqrt(reserve_a * reserve_b);

// Daily reward from annual APY (10 % = 1 000 bps)
let reward = daily_reward(1_000_000, 1_000); // โ†’ Some(273)

// Cross-asset via oracle rate (1e7 scale)
let usd_out = convert_rate(xlm_amount, rate); // โ†’ amount * rate / 1e7

๐Ÿ” crypto โ€” Hashing & Signature Verification

All operations delegate to the Soroban host via env.crypto() โ€” zero additional WASM weight:

Rust โ€” crypto module
use olighft_sdk::{sha256, keccak256, verify_ed25519, hash_bytes};

// SHA-256 of raw bytes
let digest: BytesN<32> = sha256(&env, &data);

// Keccak-256 (Ethereum-compatible)
let k_digest = keccak256(&env, &data);

// Ed25519 signature verification (traps on failure)
verify_ed25519(&env, &public_key, &message, &signature);

// Derive a namespaced hash from a byte slice
let nonce_hash = hash_bytes(&env, b"nonce:alice:42");

๐Ÿ’พ storage โ€” Typed Persistent Storage Access

Wraps all three Soroban storage tiers with generic, typed helpers. No more env.storage().persistent().get(...) chains:

Rust โ€” storage module
use olighft_sdk::{
    instance_get, instance_set, instance_get_or,
    persistent_get, persistent_set, persistent_has,
    temporary_set, temporary_get,
};

// Instance tier โ€” admin config
instance_set(&env, &DataKey::Admin, &admin);
let admin: Address = instance_get(&env, &DataKey::Admin).unwrap();

// Persistent tier โ€” user balances
persistent_set(&env, &DataKey::Balance(user.clone()), &amount);
let bal: i128 = persistent_get(&env, &key).unwrap_or(0);

// Or use get-with-default:
let total: i128 = instance_get_or(&env, &DataKey::TotalSupply, 0);

// Temporary tier โ€” nonces, caches
temporary_set(&env, &nonce_key, &1u32);

๐Ÿ”— invoke โ€” Cross-Contract Calls via Stable Identifiers

Instead of hard-coding contract addresses, store them under a ContractId enum and resolve at runtime:

Rust โ€” invoke module
use olighft_sdk::{
    transfer, token_client, invoke, invoke_void,
    ContractId, register_contract, resolve_contract,
};

// Register sibling contracts at initialisation
register_contract(&env, &ContractId::Token, &token_addr);
register_contract(&env, &ContractId::Staking, &staking_addr);

// Later: resolve and call
let token = resolve_contract(&env, &ContractId::Token).unwrap();
transfer(&env, &token, &from, &to, &amount);

// Or build a full token client
let client = token_client(&env, &token);
client.balance(&user);

// Generic invocation of any contract function
let rewards: i128 = invoke(
    &env,
    &staking_addr,
    &Symbol::new(&env, "pending_rewards"),
    (Vec::new(&env),), // args
);
๐Ÿ—‚๏ธ
Stable identifiers: The ContractId enum (Token, Staking, CardStaking, SwapAmm, Payment, Invite) stores addresses in instance storage. This decouples contracts from hard-coded IDs โ€” re-deploy a sibling and just call register_contract again.

โšก SDK CLI Tool โ€” olighft

A single binary for the entire contract lifecycle. Install with cargo install --path cli from the workspace root:

Terminal โ€” overview
# See all commands
olighft --help

# Check workspace health + tool availability
olighft config
Command Description Key Flags
olighft build Compile one or all contracts to optimised WASM -c <name>, --optimize, --soroban-optimize, -p release-with-logs
olighft test Run contract test suites with filters -c <name>, --with-logs, -f <name>, -- --nocapture
olighft inspect Show WASM size, SHA-256, exports, custom sections -c <name>, --abi, --json
olighft version Manage contract semver & changelogs show, bump -c token -l minor -m "โ€ฆ", set, log
olighft deploy Deploy to local / testnet / mainnet -n <network>, -s <source>, --dry-run, --no-build
olighft sandbox Local sandbox + REPL (identical to on-chain) --auto-deploy, --accounts <n>, --repl, --port <p>

๐Ÿ”จ olighft build โ€” Compile Contracts

Terminal
# Build all contracts with full optimisation
olighft build --optimize

# Build only the token contract
olighft build -c token

# Build with debug logs enabled
olighft build -p release-with-logs

# Build + Soroban-specific optimisation pass
olighft build --optimize --soroban-optimize

Each contract compiles to target/wasm32-unknown-unknown/release/olighft_<name>.wasm and the CLI reports the artifact size after each build.

๐Ÿงช olighft test โ€” Run Tests

Terminal
# Test all contracts
olighft test

# Test only staking, with output visible
olighft test -c staking -- --nocapture

# Run only tests matching "compound"
olighft test -f compound

# With debug assertions for detailed errors
olighft test --with-logs

๐Ÿ” olighft inspect โ€” Inspect WASM

Terminal
# Human-readable output
olighft inspect -c token

# Output:
#   file    : target/.../olighft_token.wasm
#   size    : 42.3 KiB
#   sha256  : a1b2c3d4e5f6...
#   exports (8):
#     [fn] initialize
#     [fn] transfer
#     [fn] approve
#     ...

# Machine-readable JSON (for CI pipelines)
olighft inspect -c token --json

# Full ABI via soroban/stellar CLI
olighft inspect -c token --abi

๐Ÿท๏ธ olighft version โ€” Versioning

Terminal
# Show all contract versions
olighft version show
#   token              1.0.0
#   staking            1.0.0
#   card_staking       1.0.0
#   ...

# Bump token to 1.1.0 with changelog
olighft version bump -c token -l minor -m "added burn_from function"

# Set an explicit version
olighft version set -c payment -v 2.0.0

# View changelogs
olighft version log -c token

๐Ÿš€ olighft deploy โ€” Deploy Contracts

Terminal
# Deploy all contracts to testnet (builds first)
olighft deploy -n testnet -s alice

# Deploy only payment contract, skip build
olighft deploy -c payment -n testnet -s alice --no-build

# Dry-run โ€” print commands without executing
olighft deploy -n mainnet -s deployer --dry-run

# Outputs a deploy-testnet.json manifest:
# { "network": "testnet", "contracts": [
#     { "contract": "token",   "id": "CABC..." },
#     { "contract": "staking", "id": "CDEF..." },
#     ... ] }

Deploys in dependency order: token โ†’ invite โ†’ staking โ†’ card_staking โ†’ swap_amm โ†’ payment. A JSON manifest is written after each deploy for downstream tooling.

๐Ÿ—๏ธ olighft sandbox โ€” Local Testing Mode

Spins up a complete local Soroban node with funded test accounts and an interactive REPL. The execution environment is identical to on-chain โ€” same host functions, same resource metering, same auth model.

Terminal
# Start sandbox, deploy all contracts, open REPL
olighft sandbox --auto-deploy --repl

# Output:
#   mode     : local (identical to on-chain)
#   rpc      : http://localhost:8000
#   accounts : 3
#
#   Deployed Contracts:
#     token            CABC...
#     staking          CDEF...
#     card_staking     CGHI...
#     ...

# Interactive REPL commands:
olighft> invoke token transfer --from test-account-0 --to test-account-1 --amount 1000
olighft> invoke staking pending_rewards --user test-account-0
olighft> accounts
olighft> contracts
olighft> ledger
olighft> quit
๐Ÿ’ก
Local testing mode: The SDK CLI includes a complete implementation of the contract host environment โ€” identical to the one that runs on-chain. Contracts can be run locally, tested and debugged with a standard IDE debugger, unit tested with a native test harness, and fuzzed at high speed. The sandbox also writes a sandbox-manifest.json with all deployed contract IDs and test account names.

๐Ÿ–ฅ๏ธ Host Environment

The host environment is a set of Rust crates compiled into the SDK CLI tool and stellar-core. It provides:

  1. Host objects & functions โ€” Native implementations of common operations (crypto, math, storage)
  2. On-chain storage interface โ€” Read/write persistent, instance, and temporary data
  3. Contract invocation โ€” Call other contracts via stable identifiers
  4. Resource accounting โ€” CPU, memory, and storage metering with fee charging
  5. Wasm interpreter โ€” Executes compiled contract bytecode

Most developers won't interact with the host directly โ€” SDK functions wrap its facilities and provide richer, more ergonomic types. But understanding its structure helps you understand the conceptual model the SDK presents, especially when testing or debugging contracts compiled natively on a local workstation.

๐Ÿ“ฆ Wasm Bytecode

Contracts are compiled into Wasm bytecode, which is uploaded to the ledger in a CONTRACT_DATA entry. The Stellar network executes this bytecode whenever a transaction invokes a contract function.

๐Ÿ”— Contract Instances

Multiple instances can be deployed referencing the same Wasm bytecode. This allows contracts to share code while maintaining separate storage, state, and configurations. For example, you install a token WASM once, then deploy Gold, Visa, and Platinum card contracts from the same bytecode โ€” each with its own storage.

Terminal
# Install bytecode once
stellar contract install --wasm token.wasm --network testnet --source deployer
# Output: WASM_HASH abc123...

# Deploy multiple instances from the same bytecode
stellar contract deploy --wasm-hash abc123... --network testnet --source deployer  # โ†’ Gold
stellar contract deploy --wasm-hash abc123... --network testnet --source deployer  # โ†’ Visa
stellar contract deploy --wasm-hash abc123... --network testnet --source deployer  # โ†’ Platinum

๐Ÿ’พ Storage Types

Soroban provides three storage tiers, each optimized for different data lifetimes and cost profiles:

Rust โ€” Storage API
// Instance โ€” lives as long as the contract
env.storage().instance().set(&"admin", &admin);

// Persistent โ€” long-lived, needs TTL extension
env.storage().persistent().set(&user_key, &balance);
env.storage().persistent().extend_ttl(&user_key, 100, 100);

// Temporary โ€” auto-expires, cheapest for ephemeral data
env.storage().temporary().set(&nonce_key, &nonce);

๐Ÿ” Authorization

Soroban provides robust, flexible authorization. Contracts verify signatures from users or other contracts using require_auth(). The auth framework supports:

  1. require_auth() โ€” Verify the caller signed the transaction (most common)
  2. require_auth_for_args() โ€” Verify the caller authorized specific arguments (for atomic swaps, approvals)
  3. Cross-contract auth โ€” Auth trees propagate through sub-invocations automatically
  4. Simulation โ€” stellar contract invoke --simulate-only previews auth requirements before signing
๐Ÿ“–
For the most up-to-date information on all core concepts, visit the official documentation at developers.stellar.org. For a comprehensive introduction, see the Smart Contract Learn Section. Write your first contract with the Hello World Tutorial.
โœ๏ธ

Step 1 โ€” Write Your Contract

Define terms, conditions, functions & execution rules in Rust
STEP 1 โ–ผ

Define your contract's terms, conditions, functions, and execution rules using Rust. For Stellar's Smart Contracts platform (Soroban), you'll write contracts in Rust and compile them to WebAssembly (WASM).

๐Ÿ“ Project Structure

project layout
my-soroban-contract/
โ”œโ”€โ”€ Cargo.toml          # Rust package manifest
โ”œโ”€โ”€ src/
โ”‚   โ”œโ”€โ”€ lib.rs          # Contract entry point
โ”‚   โ”œโ”€โ”€ types.rs        # Custom data types
โ”‚   โ”œโ”€โ”€ errors.rs       # Error definitions
โ”‚   โ””โ”€โ”€ storage.rs      # Storage helpers
โ”œโ”€โ”€ tests/
โ”‚   โ””โ”€โ”€ test.rs         # Integration tests
โ””โ”€โ”€ target/
    โ””โ”€โ”€ wasm32-.../
        โ””โ”€โ”€ release/
            โ””โ”€โ”€ contract.wasm  # Compiled output

๐Ÿ“ Contract Skeleton

Rust โ€” lib.rs
#![no_std]
use soroban_sdk::{
    contract, contractimpl, contracttype,
    Address, Env, Symbol, Vec, Map,
    token,
};

// Define custom types for your contract
#[contracttype]
pub enum CardTier {
    Visa,
    Mastercard,
    Gold,
    Platinum,
    Black,
}

#[contracttype]
pub struct StakePosition {
    pub owner: Address,
    pub amount: i128,
    pub lock_until: u64,
    pub apy_bps: u32,       // basis points
    pub auto_compound: bool,
    pub last_compound: u64,
}

// Contract entry point
#[contract]
pub struct SmartCardCoin;

#[contractimpl]
impl SmartCardCoin {

    /// Initialize the contract with admin + config
    pub fn initialize(
        env: Env,
        admin: Address,
        token_addr: Address,
        base_apy_bps: u32,
    ) {
        admin.require_auth();
        // Store config in instance storage
        env.storage().instance().set(&"admin", &admin);
        env.storage().instance().set(&"token", &token_addr);
        env.storage().instance().set(&"base_apy", &base_apy_bps);
    }

    /// Stake tokens with optional lock period
    pub fn stake(
        env: Env,
        user: Address,
        amount: i128,
        lock_days: u64,
        auto_compound: bool,
    ) -> StakePosition {
        user.require_auth();

        // Transfer tokens to contract
        let token_addr: Address = env.storage()
            .instance().get(&"token").unwrap();
        let client = token::Client::new(&env, &token_addr);
        client.transfer(&user, &env.current_contract_address(), &amount);

        // Calculate lock bonus
        let base_apy: u32 = env.storage()
            .instance().get(&"base_apy").unwrap();
        let lock_boost = match lock_days {
            0..=29   => 0,
            30..=89  => 150,   // +1.5%
            90..=179 => 350,   // +3.5%
            180..=364 => 600,  // +6%
            _ => 1000,            // +10%
        };

        let position = StakePosition {
            owner: user.clone(),
            amount,
            lock_until: env.ledger().timestamp() + (lock_days * 86400),
            apy_bps: base_apy + lock_boost,
            auto_compound,
            last_compound: env.ledger().timestamp(),
        };

        // Store position
        env.storage().persistent().set(&user, &position);
        position
    }

    /// Compound accrued rewards back into stake
    pub fn compound(env: Env, user: Address) -> i128 {
        let mut pos: StakePosition = env.storage()
            .persistent().get(&user).unwrap();

        let elapsed = env.ledger().timestamp() - pos.last_compound;
        let reward = (pos.amount * pos.apy_bps as i128
            * elapsed as i128)
            / (10000 * 365 * 86400);

        pos.amount += reward;
        pos.last_compound = env.ledger().timestamp();
        env.storage().persistent().set(&user, &pos);
        reward
    }
}
๐Ÿ’ก
The #![no_std] attribute is required โ€” Soroban contracts run in a WASM sandbox without access to the Rust standard library. Use soroban_sdk types instead.

๐Ÿ“ฆ Cargo.toml Configuration

TOML
[package]
name = "future-crypto-card"
version = "2.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
soroban-sdk = "21.0.0"

[dev-dependencies]
soroban-sdk = { version = "21.0.0", features = ["testutils"] }

[profile.release]
opt-level = "z"        # Minimize WASM size
overflow-checks = true # Safety first
debug = 0
strip = "symbols"
lto = true
panic = "abort"

๐Ÿ”จ Compile to WASM

$ soroban contract build

This compiles your Rust code to target/wasm32-unknown-unknown/release/future_crypto_card.wasm. The optimized binary is typically 10โ€“50 KB.

  • Created project with soroban contract init
  • Defined contract types and storage schema
  • Implemented all contract functions with require_auth()
  • Compiled to WASM successfully
๐Ÿงช

Step 2 โ€” Test Your Contract

Catch bugs before deployment in local & testnet environments
STEP 2 โ–ผ

Once written, test your smart contract in a local or testnet environment to ensure everything works correctly. This is crucial for catching bugs before deployment.

๐Ÿ  Unit Tests (Local)

Rust โ€” tests/test.rs
#[cfg(test)]
mod tests {
    use super::*;
    use soroban_sdk::{testutils::Address as _, Env};

    #[test]
    fn test_stake_and_compound() {
        let env = Env::default();
        env.mock_all_auths();

        let contract_id = env.register_contract(None, SmartCardCoin);
        let client = SmartCardCoinClient::new(&env, &contract_id);

        let admin = Address::generate(&env);
        let user  = Address::generate(&env);
        let token = env.register_stellar_asset_contract(admin.clone());

        // Initialize
        client.initialize(&admin, &token, &850); // 8.5% base APY

        // Stake 1000 tokens for 90 days with auto-compound
        let pos = client.stake(&user, &1000_0000000, &90, &true);

        assert_eq!(pos.amount, 1000_0000000);
        assert_eq!(pos.apy_bps, 850 + 350); // base + 90-day lock boost
        assert!(pos.auto_compound);

        // Advance ledger time by 30 days
        env.ledger().set(soroban_sdk::testutils::Ledger {
            timestamp: env.ledger().timestamp() + 30 * 86400,
            ..Default::default()
        });

        // Compound rewards
        let reward = client.compound(&user);
        assert!(reward > 0, "Should have accrued rewards");
    }
}
$ cargo test

๐ŸŒ Testnet Deployment

Deploy to Stellar's public testnet (Futurenet) for real-world-like testing without risking real funds:

$ soroban contract deploy --wasm target/wasm32-unknown-unknown/release/future_crypto_card.wasm --network testnet --source alice
โš ๏ธ
Always test on testnet first. Testnet XLM is free from the Friendbot faucet. Never deploy untested code to mainnet โ€” bugs in smart contracts can result in permanent loss of funds.

โœ… Testing Best Practices

  1. Unit tests for every function โ€” Test each public function in isolation using soroban_sdk::testutils
  2. Edge case testing โ€” Zero amounts, overflow values, expired locks, unauthorized callers
  3. Time-dependent logic โ€” Use env.ledger().set() to simulate time passage for staking/compounding
  4. Auth testing โ€” Verify require_auth() rejects unauthorized invocations
  5. Integration tests on testnet โ€” Test with real network conditions, latency, and concurrent transactions
  6. Fuzz testing โ€” Use property-based testing with proptest for random input generation
  7. Gas profiling โ€” Check resource consumption with soroban contract invoke --cost
  8. Security audit โ€” Have at least one independent review before mainnet deployment
  • All unit tests passing with cargo test
  • Edge cases and auth failures tested
  • Deployed and tested on Stellar testnet
  • Gas costs profiled and within budget
๐Ÿš€

Step 3 โ€” Deploy to Mainnet

Submit compiled WASM to Stellar mainnet and go live
STEP 3 โ–ผ

Deploy the smart contract to the blockchain's mainnet by submitting a transaction with the compiled contract code. This makes your contract live and accessible on the network.

๐Ÿ“‹ Pre-Deployment Checklist

  • All tests green โ€” Unit + integration + testnet
  • WASM optimized โ€” Built with opt-level = "z" and lto = true
  • Security audit completed โ€” At least one independent review
  • Admin keys secured โ€” Multi-sig (3-of-5) configured
  • Sufficient XLM โ€” For deployment fees + storage rent
  • Contract verified โ€” Source code published for reproducible builds

1๏ธโƒฃ Install the Compiled WASM

First, install (upload) the WASM bytecode to the network:

Terminal
# Install WASM on mainnet
soroban contract install \
  --wasm target/wasm32-unknown-unknown/release/future_crypto_card.wasm \
  --network mainnet \
  --source deployer-key

# Output: WASM hash (e.g., abc123...)

2๏ธโƒฃ Deploy a Contract Instance

Terminal
# Deploy contract instance from installed WASM
soroban contract deploy \
  --wasm-hash abc123... \
  --network mainnet \
  --source deployer-key

# Output: Contract ID (e.g., CDLZ...X7KQ)

3๏ธโƒฃ Initialize the Contract

Terminal
# Call initialize with admin, token address, and base APY
soroban contract invoke \
  --id CDLZ...X7KQ \
  --network mainnet \
  --source admin-key \
  -- initialize \
  --admin GDQP...ADMIN \
  --token_addr CCUS...USDC \
  --base_apy_bps 850
๐Ÿ”—
Verify your deployment โ€” After deploying, verify the contract WASM hash matches your local build:
soroban contract info --id CDLZ...X7KQ --network mainnet

๐Ÿ’ฐ Deployment Costs

OPERATION EST. COST
WASM Install (one-time)~1โ€“5 XLM
Contract Deploy~0.5โ€“2 XLM
Initialize Call~0.01 XLM
Storage Rent (annual)~0.1โ€“1 XLM
  • WASM installed on mainnet
  • Contract instance deployed
  • Contract initialized with correct parameters
  • Deployment verified on Stellar Explorer
โšก

Step 4 โ€” Invoke & Interact

Send transactions that trigger contract execution on-chain
STEP 4 โ–ผ

After deployment, others can interact with your contract by sending transactions that invoke its functions. These interactions trigger the contract's execution, and the blockchain network validates and records the resulting state changes.

๐Ÿ–ฅ๏ธ CLI Invocation

Terminal
# Stake 500 XLM for 90 days with auto-compound
soroban contract invoke \
  --id CDLZ...X7KQ \
  --network mainnet \
  --source user-key \
  -- stake \
  --user GBUY...USER \
  --amount 5000000000 \
  --lock_days 90 \
  --auto_compound true

# Compound rewards
soroban contract invoke \
  --id CDLZ...X7KQ \
  --network mainnet \
  --source user-key \
  -- compound \
  --user GBUY...USER

# Read balance (no auth needed)
soroban contract invoke \
  --id CDLZ...X7KQ \
  --network mainnet \
  -- get_balance \
  --user GBUY...USER

๐ŸŒ JavaScript SDK Integration

JavaScript
import { Contract, Server, TransactionBuilder } from '@stellar/stellar-sdk';

const server = new Server('https://soroban-rpc.mainnet.stellar.gateway.fm');
const contract = new Contract('CDLZ...X7KQ');

// Build a stake transaction
async function stakeTokens(userKey, amount, lockDays) {
  const account = await server.getAccount(userKey.publicKey());

  const tx = new TransactionBuilder(account, {
    fee: '100',
    networkPassphrase: 'Public Global Stellar Network ; September 2015',
  })
  .addOperation(
    contract.call(
      'stake',
      userKey.publicKey(),  // user
      amount,               // i128
      lockDays,             // u64
      true,                 // auto_compound
    )
  )
  .setTimeout(30)
  .build();

  // Simulate โ†’ sign โ†’ submit
  const simulated = await server.simulateTransaction(tx);
  const prepared  = assembleTransaction(tx, simulated);
  prepared.sign(userKey);
  const result = await server.sendTransaction(prepared);
  return result;
}
  • Successfully invoked contract via CLI
  • Integrated with JavaScript SDK
  • Verified state changes on Stellar Explorer
  • Tested auto-compound lifecycle end-to-end
๐ŸŽฌ

Video Tutorial

Watch the full walkthrough of building your first Soroban smart contract on Stellar.

๐Ÿ’ป

Local Development Environment Setup

Everything you need to start building Soroban contracts โ€” Rust, CLI, editor, networks & the OLIGHFT toolkit
โ–ผ

๐Ÿ“‹ Prerequisites

Requirement Minimum Check
Rust toolchain 1.74+ rustc --version
WASM target wasm32-unknown-unknown rustup target list --installed
Stellar CLI 21.x+ stellar --version
C/C++ linker gcc / clang / MSVC cc --version or MSVC via Build Tools
Git 2.x+ git --version

1๏ธโƒฃ Install Rust

Rust is installed via rustup โ€” a version manager that handles the compiler, Cargo, and compilation targets.

๐Ÿง macOS / Linux
$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Follow the prompts, then restart your terminal or run source $HOME/.cargo/env.

๐ŸชŸ Windows

Download and run rustup-init.exe from rustup.rs.

โš ๏ธ
Requires Visual Studio Build Tools with "Desktop development with C++" workload. Without this, native test compilation will fail with linker errors.
Terminal โ€” add WASM target
# Add the WebAssembly compilation target (required for contract builds)
rustup target add wasm32-unknown-unknown

# Keep your toolchain up to date
rustup update

2๏ธโƒฃ Install the Stellar CLI

The Stellar CLI (stellar, formerly soroban) is the official tool for building, deploying, and invoking contracts.

Terminal โ€” install CLI
# Install the latest Stellar CLI
cargo install --locked stellar-cli

# Verify the installation
stellar --version
# โ†’ stellar-cli 21.X.X

# The older `soroban` command still works as an alias
# but `stellar` is the canonical name going forward
๐Ÿ“
CLI rename: The CLI was renamed from soroban-cli to stellar-cli. Both stellar and soroban commands work. All OLIGHFT tooling detects either automatically. This guide uses the stellar name throughout.

3๏ธโƒฃ Install the OLIGHFT CLI (optional)

The project-specific CLI wraps Cargo + Stellar CLI with build, test, inspect, version, deploy, and sandbox commands tailored to the OLIGHFT workspace.

Terminal โ€” install OLIGHFT CLI
# From the contracts/ workspace root:
cargo install --path cli

# Or build without installing globally:
cargo build -p olighft-cli --release
# Binary at: target/release/olighft(.exe)

# Verify
olighft --version
olighft config    # shows workspace health + toolchain status
olighft command Equivalent manual commands
olighft build cargo build --target wasm32-unknown-unknown --release -p <pkg>
olighft test cargo test -p <pkg>
olighft inspect -c token stellar contract inspect --wasm <file>
olighft deploy -n testnet stellar contract deploy (ร— 6 contracts)
olighft sandbox --repl stellar container start + manual account setup

4๏ธโƒฃ Configure Networks

Register Testnet and Mainnet so the CLI knows where to send transactions:

Terminal โ€” network setup
# Add Stellar Testnet
stellar network add testnet \
  --rpc-url https://soroban-testnet.stellar.org \
  --network-passphrase "Test SDF Network ; September 2015"

# Add Stellar Mainnet
stellar network add mainnet \
  --rpc-url https://soroban-rpc.mainnet.stellar.gateway.fm \
  --network-passphrase "Public Global Stellar Network ; September 2015"

# List configured networks
stellar network ls
# โ†’ testnet    https://soroban-testnet.stellar.org
# โ†’ mainnet    https://soroban-rpc.mainnet.stellar.gateway.fm

5๏ธโƒฃ Create a Test Identity & Fund It

Identities are local keypairs stored by the CLI. Friendbot funds them with free testnet XLM:

Terminal โ€” identity setup
# Generate a new identity (stores keypair locally)
stellar keys generate alice --network testnet

# View the public address
stellar keys address alice
# โ†’ GBXYZ...ABC

# Fund from Friendbot (10,000 free testnet XLM)
stellar keys fund alice --network testnet

# Verify the balance
stellar account show --network testnet --source alice

# Create additional test accounts if needed
stellar keys generate bob --network testnet
stellar keys fund bob --network testnet
๐Ÿ”‘
Security: Keys generated with stellar keys generate are stored unencrypted on disk. This is fine for testnet development, but never use a locally-stored key for mainnet funds. For production deployments, use hardware wallets or a secure key management service.

6๏ธโƒฃ Editor & IDE Configuration

A properly configured editor catches 90% of Soroban errors before you compile:

๐ŸŸฆ VS Code (recommended)
๐ŸŸง IntelliJ / CLion
  • โœ“ Rust plugin โ€” bundled in CLion, or install in IntelliJ IDEA
  • โœ“ TOML plugin โ€” Cargo.toml highlighting
  • โ—‹ Enable "expand macros" for #[contractimpl] insight
.vscode/settings.json โ€” recommended
{
  // Point rust-analyzer to the WASM target for accurate diagnostics
  "rust-analyzer.cargo.target": "wasm32-unknown-unknown",

  // Enable all features for SDK test utilities
  "rust-analyzer.cargo.features": "all",

  // Check on save (fast feedback loop)
  "rust-analyzer.check.command": "clippy",

  // Inline type hints (helpful for Soroban env types)
  "rust-analyzer.inlayHints.typeHints.enable": true,

  // Exclude build artifacts from file watcher
  "files.watcherExclude": {
    "**/target/**": true
  }
}

7๏ธโƒฃ Create Your First Project

Terminal โ€” scaffold & build
# Scaffold a new Soroban project
stellar contract init my-first-contract
cd my-first-contract

# Build to WASM
cargo build --target wasm32-unknown-unknown --release
# โ†’ target/wasm32-unknown-unknown/release/my_first_contract.wasm

# Run tests
cargo test

# Or use the OLIGHFT CLI (from the OLIGHFT workspace):
olighft build
olighft test

โœ… Verification Checklist

Run these commands to confirm your environment is fully set up:

Terminal โ€” verify all
# Rust compiler
rustc --version                           # โ†’ rustc 1.7X.0

# Cargo build tool
cargo --version                           # โ†’ cargo 1.7X.0

# WASM target installed
rustup target list --installed | grep wasm # โ†’ wasm32-unknown-unknown

# Stellar CLI
stellar --version                         # โ†’ stellar-cli 21.X.X

# Networks configured
stellar network ls                        # โ†’ testnet, mainnet

# Identity exists and is funded
stellar keys address alice                # โ†’ GBXYZ...

# Build works end-to-end
cargo build --target wasm32-unknown-unknown --release

# (Optional) OLIGHFT CLI
olighft config                            # โ†’ shows workspace + toolchain
  • Rust installed โ€” rustc --version prints 1.74+
  • WASM target added โ€” wasm32-unknown-unknown in installed list
  • Stellar CLI installed โ€” stellar --version prints 21.x+
  • Testnet configured โ€” stellar network ls shows testnet
  • Identity created & funded โ€” stellar keys address alice returns a key
  • Editor configured โ€” rust-analyzer running, inline type hints visible
  • Build works โ€” cargo build --target wasm32-unknown-unknown --release succeeds
  • Tests pass โ€” cargo test returns green

๐Ÿ”ง Troubleshooting

error: linker 'cc' not found on Windows โ–ผ
Install Visual Studio Build Tools and select the "Desktop development with C++" workload. Restart your terminal after installation. This provides the MSVC linker that Rust needs for native test binaries.
error[E0463]: can't find crate for 'std' when building for WASM โ–ผ
Your contract file is missing #![no_std] at the top. Soroban contracts cannot use the Rust standard library โ€” add #![no_std] as the first line and use SDK types (Vec, Map, String) from soroban_sdk instead.
error: target 'wasm32-unknown-unknown' not found โ–ผ
Run rustup target add wasm32-unknown-unknown. If you have multiple toolchains, specify: rustup target add wasm32-unknown-unknown --toolchain stable.
rust-analyzer shows red squiggles on #[contract] / #[contractimpl] โ–ผ
Set "rust-analyzer.cargo.target": "wasm32-unknown-unknown" in your VS Code settings. Also run cargo check in the terminal first โ€” rust-analyzer sometimes needs the initial build metadata before it can resolve proc-macro attributes.
stellar command not found after cargo install โ–ผ
Ensure ~/.cargo/bin (Linux/macOS) or %USERPROFILE%\.cargo\bin (Windows) is in your PATH. Restart your terminal or run source $HOME/.cargo/env. On Windows you can also add it via System โ†’ Environment Variables.
Friendbot says "account already exists" โ–ผ
The identity was already funded. This is fine โ€” your account has testnet XLM. Run stellar account show --network testnet --source alice to see the balance. If you actually need a fresh account, generate a new identity with a different name.

๐Ÿ”— Recommended Tools & Resources

๐Ÿ“š

Example Contracts

24 official examples from the Stellar team — ordered from foundational to advanced
โ–ผ

The examples below are listed sequentially — earlier contracts build a foundation of concepts required for the later ones. You’re free to jump ahead, but the order is intentional.

๐Ÿงฑ Foundational

๐Ÿ“ˆ
Storage
Increment a counter and store the incremented value
๐Ÿ“ก
Events
Publish events from a smart contract
๐Ÿ“
Custom Types
Define your own data structures in a smart contract
โŒ
Errors
Define and generate errors in a smart contract
๐Ÿ’ฌ
Logging
Debug a smart contract with logs
๐Ÿงฐ
Workspace
Develop multiple contracts side-by-side in one project

๐Ÿ” Accounts & Authorization

๐Ÿ”‘
Auth
Implement authentication and authorization
๐Ÿ‘ค
Simple Account
Minimal contract account secured by a single ed25519 public key
๐Ÿ‘ฅ
Complex Account
Contract account with multisig and custom authorization policies
๐Ÿ”
BLS Signature
Contract account with BLS signature verification

๐Ÿ”„ Cross-Contract & Deployment

๐Ÿ“ž
Cross Contract Calls
Call a smart contract from another smart contract
๐Ÿš€
Deployer
Deploy and initialize a contract using another contract
โฌ†๏ธ
Upgradeable Contract
Upgrading Wasm bytecode for a deployed contract
๐Ÿง 
Allocator
Use the allocator feature to emulate heap memory

๐Ÿ’ฑ Tokens & DeFi

๐Ÿช™
Tokens
Write a CAP-46-6 compliant token contract
๐ŸŸข
Fungible Token
OpenZeppelin audited contract for fungible tokens on Stellar
๐Ÿ‡บ
Non-Fungible Token
OpenZeppelin audited contract for NFTs on Stellar
โš–๏ธ
Atomic Swap
Swap tokens atomically between authorized users
๐Ÿ‘ฅโš–๏ธ
Batched Atomic Swaps
Swap a token pair among groups of authorized users
๐ŸŒŠ
Liquidity Pool
Write a constant-product liquidity pool contract
๐Ÿ”’
Mint Lock
Delegate minting with limits via a contract
โฐ
Timelock
Lock tokens to be claimed by another user under set conditions
๐Ÿ 
Single Offer Sale
Make a standing offer to sell a token for another token

๐Ÿงช Testing

๐Ÿ”€
Fuzz Testing
Increase confidence in correctness with fuzz testing
๐Ÿ’ก
Each example has an accompanying tutorial that walks through the contract design. View all examples on the Stellar Developer Docs.
๐Ÿช™

Assets Overview & Comparison

Stellar Assets vs SEP-41 Contract Tokens vs SEP-57 T-REX Tokens
โ–ผ
โ„น๏ธ
The term “custom token” has been deprecated in favor of “contract token”. See the discussion in the Stellar Developer Discord.

This comparison covers the three primary tokenization models on Stellar to help issuers select the most appropriate model:

MODEL 1
Stellar Assets
Trustlines + Operations + SEP-41 via built-in Stellar Asset Contract (SAC)
MODEL 2
SEP-41 Contract Tokens
SEP-41 interface implemented by a smart contract — fully extensible
MODEL 3
SEP-57 T-REX Tokens
Token for Regulated EXchanges — SEP-41 with extensible compliance logic

๐Ÿ“Š Tokenization Model Comparison

DIMENSION STELLAR ASSET (SAC) SEP-41 CONTRACT TOKEN SEP-57 T-REX
Token Implementation Trustlines + Operations + SEP-41 via SAC SEP-41 interface via smart contract, fully extensible SEP-41 with extensible compliance logic
Programmability โŒ Protocol-defined โœ… Fully customizable โœ… Fully customizable + compliance rules
Interaction Method Native ops (accounts) and contract calls (via SAC) Contract calls Contract calls
Admin Control Issuer flags + optional admin via SAC Custom contract logic Built-in compliance and rule enforcement
Cost & Speed Very low (native) / Moderate (via SAC) Moderate Higher
Ecosystem Stellar payments, DEX, and smart contracts Stellar DeFi & dApps / Interop with other L1s Institutional DeFi / Interop with other L1s
Trustline Required โœ… Yes* โŒ No โŒ No
Ledger Storage 0.5 XLM per trustline, no expiry* Rent and Archives Rent and Archives
Ideal For Payments, simple assets (fiat stablecoins), Stellar smart contracts DeFi and custom tokenomics Institutional RWAs with compliance
Source Code Built-in SAC OZ Fungible Token OZ T-REX (RWA)
Relevant SEPs SEP-0001 (Stellar Info File)
SEP-0014 (Dynamic Asset Metadata)
SEP-41 (Soroban Token Interface)
SEP-41 (Soroban Token Interface) SEP-41 (Soroban Token Interface)
SEP-0057 (T-REX / Token for Regulated EXchanges)
โšก
Protocol 26 Note: By assigning a smart contract (C... address) as the owner of a Stellar Asset Contract, balances and transfer rules can be fully managed within the contract — bridging the gap between native assets and contract tokens.
๐Ÿ›ก๏ธ

Best Practices & Security

Production-ready contract development guidelines
โ–ผ

๐Ÿ” Security Practices

Smart contracts are immutable once deployed. A single vulnerability can drain all funds with no recourse. Follow these rules as non-negotiable minimums.

1 Always use require_auth()

Every function that modifies state must authenticate the caller. Without this, anyone can invoke the function on behalf of any address.

Rust โ€” auth pattern
// โœ… CORRECT โ€” caller must sign the transaction
pub fn transfer(env: Env, from: Address, to: Address, amount: i128) {
    from.require_auth();          // โ† must come BEFORE any state change
    // ... transfer logic
}

// โŒ WRONG โ€” no auth check, anyone can drain funds
pub fn transfer(env: Env, from: Address, to: Address, amount: i128) {
    // missing require_auth()!
    do_transfer(&env, &from, &to, amount);
}
๐Ÿ’ก
For admin-only functions, store the admin address and check: let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap(); admin.require_auth();
2 Check arithmetic overflow

Rust release builds disable overflow checks by default, which means i128::MAX + 1 silently wraps to a negative number. Enable them explicitly:

Cargo.toml โ€” release profile
[profile.release]
opt-level     = "z"
lto           = true
strip         = "symbols"
overflow-checks = true   # โ† panics on overflow instead of wrapping

[profile.release-with-logs]
inherits        = "release"
debug-assertions = true
overflow-checks  = true

Additionally, prefer checked_add() / checked_mul() for critical calculations where you need to return a user-friendly error instead of a panic.

3 Validate all inputs at the boundary

Public contract functions are the system boundary. Validate every parameter before touching storage:

Rust โ€” input validation
pub fn stake(env: Env, user: Address, amount: i128) {
    user.require_auth();

    // Validate at the boundary โ€” fail fast
    if amount <= 0 {
        panic_with_error!(&env, Error::InvalidAmount);
    }
    if amount > MAX_STAKE {
        panic_with_error!(&env, Error::ExceedsLimit);
    }

    // Safe to proceed โ€” amount is guaranteed positive and in range
    do_stake(&env, &user, amount);
}
4 Limit storage writes

Each .set() call costs gas and incurs ongoing rent. Group related data into a single struct to reduce the number of writes:

Rust โ€” batched vs unbatched
// โŒ EXPENSIVE โ€” 3 separate storage writes
env.storage().persistent().set(&DataKey::Balance(user.clone()), &new_bal);
env.storage().persistent().set(&DataKey::LastStake(user.clone()), &now);
env.storage().persistent().set(&DataKey::Rewards(user.clone()), &rewards);

// โœ… CHEAPER โ€” one write with a struct
#[contracttype]
pub struct UserState { balance: i128, last_stake: u64, rewards: i128 }

env.storage().persistent().set(&DataKey::User(user), &state);
5 Use multi-sig for admin operations

A single admin key is a single point of failure. If the key is compromised, the attacker controls the entire contract. Use a multi-signature account that requires M-of-N signatures for critical operations:

  • Pause / unpause โ€” 2-of-3 signers
  • Upgrade contract โ€” 3-of-5 signers + timelock
  • Transfer admin โ€” all signers
  • Change fee parameters โ€” 2-of-3 signers
๐Ÿ’ก
Stellar supports native multi-sig at the account level via signers + thresholds. Set the admin address to a multi-sig account โ€” no extra contract code needed.
6 Implement circuit breakers

An emergency pause mechanism lets you halt the contract if an exploit is discovered:

Rust โ€” pause pattern
pub fn pause(env: Env, admin: Address) {
    admin.require_auth();
    require_admin(&env, &admin);
    env.storage().instance().set(&DataKey::Paused, &true);
    events::emit_paused(&env, admin);
}

fn require_not_paused(env: &Env) {
    let paused: bool = env.storage().instance()
        .get(&DataKey::Paused)
        .unwrap_or(false);
    if paused {
        panic_with_error!(env, Error::ContractPaused);
    }
}

// Call at the start of every user-facing function:
pub fn stake(env: Env, user: Address, amount: i128) {
    require_not_paused(&env);  // โ† circuit breaker
    user.require_auth();
    // ...
}

Add a timelock to unpause (e.g., minimum 24-hour delay) to prevent an attacker who compromises the admin key from immediately resuming a drained contract.

7 Avoid reentrancy patterns

Always follow the checks โ†’ effects โ†’ interactions (CEI) pattern. Update your own state before calling external contracts:

Rust โ€” CEI pattern
pub fn withdraw(env: Env, user: Address, amount: i128) {
    user.require_auth();

    // 1. CHECKS โ€” validate inputs
    let balance = get_balance(&env, &user);
    if amount > balance { panic_with_error!(&env, Error::InsufficientFunds); }

    // 2. EFFECTS โ€” update OUR state FIRST
    set_balance(&env, &user, balance - amount);

    // 3. INTERACTIONS โ€” external call LAST
    token_client(&env).transfer(
        &env.current_contract_address(),
        &user,
        &amount,
    );
}
โš ๏ธ
If you update state after the external call, a malicious contract could re-enter your function, read the stale balance, and drain funds. Soroban's execution model mitigates some classic reentrancy vectors, but the CEI pattern remains the correct defensive practice.
  • require_auth() on every state-changing function
  • overflow-checks = true in [profile.release]
  • Input validation at every public function entry
  • Struct batching to minimize storage writes
  • Multi-sig admin account with threshold enforcement
  • Emergency pause function with timelock on unpause
  • CEI pattern โ€” checks โ†’ effects โ†’ interactions

โšก Performance Optimization

Soroban charges for CPU instructions, memory, storage reads/writes, and WASM binary size. Every optimization directly reduces user costs.

1 Minimize WASM binary size

A smaller binary means lower deploy costs and faster startup. Apply all of these in your release profile:

Cargo.toml โ€” optimized release
[profile.release]
opt-level       = "z"        # Optimize for size (not speed)
lto             = true       # Link-time optimization โ€” eliminates dead code
codegen-units   = 1          # Single codegen unit โ€” better inlining
strip           = "symbols"  # Remove debug symbols from the binary
panic           = "abort"    # No unwind tables โ€” smaller binary
overflow-checks = true       # Keep this for safety

Additionally, run wasm-opt -Oz on the output. The OLIGHFT CLI does this automatically with olighft build --optimize. Typical reduction: 30โ€“50% smaller than unoptimized builds.

2 Batch storage operations

Every .get() and .set() call is metered. Read all the data you need into local variables, process, then write back once:

Rust โ€” batched read/write
// Read once
let mut state: UserState = env.storage().persistent()
    .get(&DataKey::User(user.clone()))
    .unwrap_or_default();

// Compute in memory (free โ€” no metering on local vars)
state.balance += amount;
state.last_stake = env.ledger().timestamp();
state.rewards = calculate_pending(&env, &state);

// Write once
env.storage().persistent().set(&DataKey::User(user), &state);
3 Use temporary storage for ephemeral data

Temporary entries have the lowest rent cost and auto-expire. Use them for data that doesn't need to survive long:

Use Case Storage Tier Why
Nonces temporary() One-time use, can expire safely
Rate-limit counters temporary() Reset naturally when TTL expires
Session tokens temporary() Short-lived by design
User balances persistent() Must survive โ€” restorable after archival
Contract config instance() Lives with the contract โ€” no separate TTL
4 Extend TTL proactively

Bump persistent entry lifetimes on every interaction, so active users never lose data to archival:

Rust โ€” TTL extension pattern
const DAY_IN_LEDGERS: u32 = 17_280;            // ~5 sec/ledger ร— 17,280 โ‰ˆ 1 day
const BUMP_THRESHOLD: u32 = 30 * DAY_IN_LEDGERS; // extend when < 30 days left
const BUMP_TO:        u32 = 120 * DAY_IN_LEDGERS; // extend to 120 days

fn bump_user(env: &Env, key: &DataKey) {
    env.storage().persistent().extend_ttl(key, BUMP_THRESHOLD, BUMP_TO);
}

// Also bump the contract instance itself:
env.storage().instance().extend_ttl(BUMP_THRESHOLD, BUMP_TO);
๐Ÿ“
Cost tradeoff: Extending TTL costs gas proportional to the extension length and entry size. Calling extend_ttl with a threshold means it only actually extends when the remaining TTL drops below that threshold โ€” avoiding unnecessary cost on frequent interactions.
5 Prefer i128 for token amounts

i128 is Soroban's native numeric type โ€” it serializes and deserializes without conversion overhead. Using u64 or custom big-integer types adds unnecessary XDR encoding cost. The soroban-token-sdk standard token interface already uses i128 for all amounts.

  • Release profile uses opt-level = "z", LTO, strip, and codegen-units = 1
  • WASM optimized with wasm-opt or olighft build --optimize
  • Struct batching โ€” related fields in one storage entry
  • Ephemeral data uses temporary() storage tier
  • TTL extensions on every user interaction with threshold guard
  • i128 amounts โ€” no custom big-int types

๐Ÿ“‹ Testing Checklist

Ship confidently by covering every layer โ€” from pure logic to live network integration.

Layer What to Test Tool When
Unit Individual function logic, edge cases, error paths cargo test Every commit
Integration Multi-contract workflows (stake โ†’ compound โ†’ withdraw) stellar CLI + testnet Before deploy
Fuzz Random inputs, boundary values, malformed data cargo-fuzz / proptest Critical paths
Gas Profiling CPU instructions, memory, read/write bytes stellar contract invoke --cost After optimization
Security Audit Auth bypass, overflow, reentrancy, fund drainage Manual review + cargo-audit Before mainnet
Rust โ€” unit test example
use soroban_sdk::{testutils::Address as _, Address, Env};

#[test]
fn test_stake_rejects_zero_amount() {
    let env = Env::default();
    env.mock_all_auths();

    let contract_id = env.register_contract(None, StakingContract);
    let client = StakingContractClient::new(&env, &contract_id);
    let user = Address::generate(&env);

    // Zero stake should panic
    let result = client.try_stake(&user, &0);
    assert!(result.is_err(), "should reject zero amount");
}

#[test]
fn test_withdraw_requires_auth() {
    let env = Env::default();
    // Do NOT mock auths โ€” test real auth enforcement

    let contract_id = env.register_contract(None, StakingContract);
    let client = StakingContractClient::new(&env, &contract_id);
    let attacker = Address::generate(&env);
    let victim = Address::generate(&env);

    // Attacker trying to withdraw victim's funds should fail
    let result = client.try_withdraw(&victim, &1000);
    assert!(result.is_err(), "should fail without auth");
}

#[test]
fn test_overflow_does_not_wrap() {
    let env = Env::default();
    env.mock_all_auths();

    let contract_id = env.register_contract(None, StakingContract);
    let client = StakingContractClient::new(&env, &contract_id);
    let user = Address::generate(&env);

    // Staking i128::MAX should not silently overflow
    let result = client.try_stake(&user, &i128::MAX);
    assert!(result.is_err(), "should reject overflow-prone amount");
}
100%
Auth paths tested
100%
Error paths tested
โ‰ฅ 80%
Line coverage
0
Clippy warnings
  • Unit tests โ€” all public functions, happy path + error cases
  • Auth tests โ€” without mock_all_auths() to verify real enforcement
  • Edge cases โ€” zero, negative, i128::MAX, empty addresses
  • Multi-step workflows โ€” stake โ†’ wait โ†’ compound โ†’ withdraw
  • Gas profiling โ€” stellar contract invoke --cost within limits
  • Fuzz testing โ€” random inputs on math-heavy functions
  • Testnet deploy โ€” full integration smoke test on live network
  • Security review โ€” independent audit or peer review completed

โ“ Frequently Asked Questions

What are the best practices for testing smart contracts before deploying to mainnet?โ–ผ

Phase 1 โ€” Unit Tests (local)

Start with comprehensive unit tests using soroban_sdk::testutils. Every public function needs at minimum a happy-path test, an error-path test, and an auth-enforcement test. Cover these edge cases explicitly:

  • Zero & negative amounts โ€” confirm they are rejected, not silently accepted
  • Expired locks / time gates โ€” use env.ledger().set() to advance time and test lock expiry
  • Unauthorized callers โ€” run without mock_all_auths() to verify real auth enforcement
  • Overflow boundaries โ€” test with i128::MAX and values near limits
  • Paused state โ€” ensure circuit breaker blocks all user operations
Rust โ€” time-dependent test pattern
use soroban_sdk::testutils::{Address as _, Ledger, LedgerInfo};

#[test]
fn test_compound_after_lock_expires() {
    let env = Env::default();
    env.mock_all_auths();

    // Setup: stake with a 7-day lock
    let user = Address::generate(&env);
    client.stake(&user, &10_000_0000000);

    // Advance ledger time by 7 days + 1 second
    env.ledger().set(LedgerInfo {
        timestamp: env.ledger().timestamp() + 7 * 86_400 + 1,
        ..env.ledger().get()
    });

    // Compound should now succeed
    let result = client.compound(&user);
    assert!(result > 0, "should earn rewards after lock period");
}

Run locally:

$ cargo test # all contracts
$ cargo test -p staking # single contract
$ cargo test -- --nocapture # with println! output
$ olighft test --with-logs # OLIGHFT CLI shortcut

Phase 2 โ€” Integration Tests (testnet)

Deploy to Stellar testnet for integration testing with real network conditions โ€” actual consensus, real gas metering, and authentic RPC latency. Use the Friendbot faucet for free testnet XLM:

# 1. Fund a test account
$ stellar keys generate deployer --network testnet
$ stellar keys fund deployer --network testnet

# 2. Deploy the contract
$ stellar contract deploy \
--wasm target/wasm32-unknown-unknown/release/staking.wasm \
--network testnet --source deployer

# 3. Test the full user flow
$ stellar contract invoke --id <CONTRACT_ID> --network testnet \
--source deployer -- initialize --admin deployer
$ stellar contract invoke --id <CONTRACT_ID> --network testnet \
--source deployer -- stake --user deployer --amount 10000000000
$ stellar contract invoke --id <CONTRACT_ID> --network testnet \
--source deployer -- compound --user deployer
$ stellar contract invoke --id <CONTRACT_ID> --network testnet \
--source deployer -- withdraw --user deployer --amount 5000000000

Test the complete user flow: initialize โ†’ stake โ†’ compound โ†’ withdraw. Verify balances change correctly, events are emitted, and TTL extensions happen on every call.

Phase 3 โ€” Gas Profiling & Security Review

Profile resource consumption to ensure your contract stays within Soroban's per-transaction limits:

$ stellar contract invoke --id <CONTRACT_ID> --network testnet \
--source deployer --cost -- stake --user deployer --amount 100000

# Output includes:
# CPU instructions: ~2,500,000 / 100,000,000 limit
# Memory bytes: ~150,000 / 40,000,000 limit
# Read bytes: ~8,000
# Write bytes: ~1,200
# Ledger entries: 4 read / 2 write
Resource Per-Tx Limit Target
CPU instructions 100,000,000 < 10,000,000 (10%)
Memory 40 MB < 4 MB (10%)
Read ledger entries 40 < 10
Write ledger entries 25 < 5

Get at least one independent security review before going to mainnet. Focus areas: auth bypass, integer overflow, storage key collisions, reentrancy, and fund drainage paths. Use cargo audit to scan dependencies for known CVEs.

How do I set up a local development environment for Soroban smart contracts?โ–ผ

1. Install Rust: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh then add the WASM target: rustup target add wasm32-unknown-unknown

2. Install the Stellar CLI: cargo install --locked stellar-cli (the CLI was renamed from soroban-cli; both stellar and soroban commands work)

3. Configure networks: Add testnet and mainnet RPC endpoints with stellar network add

4. Generate keys: stellar keys generate alice --network testnet and fund with stellar keys fund alice --network testnet

5. Scaffold a project: stellar contract init my-contract to get a working starter template

Recommended IDE: VS Code with the rust-analyzer extension. Set "rust-analyzer.cargo.target": "wasm32-unknown-unknown" for accurate inline diagnostics.

See the Full Dev Environment Setup section for detailed platform-specific instructions, troubleshooting, and verification checklists.

What is the difference between simple staking and compound staking?โ–ผ

Simple staking: You earn rewards on your original deposit only. If you stake 1000 XLM at 8.5% APY, you earn 85 XLM per year regardless of accumulated rewards.

Compound staking: Rewards are automatically reinvested, so you earn rewards on your original deposit plus all previously earned rewards. With daily compounding at 8.5%, 1000 XLM grows to ~1,088.7 XLM after one year โ€” an effective rate of 8.87%.

The more frequently you compound, the higher the effective APY. Our Soroban contract supports auto-compounding at 12h, daily, weekly, or monthly intervals.

How does storage rent work on Soroban?โ–ผ

Soroban uses a rent-based storage model. Every persistent storage entry has a TTL (time-to-live) measured in ledger sequences. When the TTL expires, the entry becomes archived and must be restored (with a fee) before it can be read or written.

To prevent expiry, contracts should call env.storage().persistent().extend_ttl() on important entries during each interaction. Our contract automatically extends entry TTLs to a minimum of 120 days on every stake, compound, or withdrawal operation.

Rent costs are based on entry size and TTL duration. Typical cost: ~0.01โ€“0.1 XLM per entry per year.

Can I upgrade a deployed Soroban contract?โ–ผ

Yes, if the contract includes an upgrade function. The pattern is: install new WASM on-chain, then invoke the contract's upgrade function with the new WASM hash. The contract's address and storage remain the same โ€” only the bytecode changes.

Our contract uses a 7-day timelock + 3-of-5 multi-sig requirement for upgrades to ensure security and give users time to review changes before they take effect.

What is Soroban to Stellar? Is it a new blockchain?โ–ผ

No. Soroban is the smart contracts platform that runs directly on the existing Stellar network. It is not a separate blockchain, sidechain, or Layer 2 โ€” contracts execute natively on Stellar validators alongside all other Stellar operations (payments, DEX trades, etc.).

How do I invoke a Soroban contract on Stellar?โ–ผ

You invoke a contract by submitting a transaction with an InvokeHostFunction operation that specifies the contract ID, function name, and arguments. This can be done via the Stellar CLI (stellar contract invoke), the JavaScript SDK (@stellar/stellar-sdk), or the Stellar Laboratory web interface.

Can Soroban contracts use Stellar accounts for authentication?โ–ผ

Yes. Soroban contracts can authenticate both Stellar accounts (G... addresses) and contract addresses (C... addresses) using the require_auth() framework. Stellar account signers (including multi-sig configurations) are automatically recognized by the auth system.

Can Soroban contracts interact with Stellar assets?โ–ผ

Yes. Every classic Stellar asset (XLM, USDC, etc.) is automatically accessible via the Stellar Asset Contract (SAC). The SAC wraps classic assets with a standard Soroban token interface, so contracts can call transfer, balance, approve, and other token functions on any Stellar asset.

Do asset issuers maintain AUTH_REQUIRED, AUTH_REVOCABLE, AUTH_CLAWBACK in Soroban?โ–ผ

Yes. Issuer authorization flags (AUTH_REQUIRED, AUTH_REVOCABLE, AUTH_CLAWBACK) are respected when assets are held by non-account identifiers in Soroban. The issuer retains full control over their asset regardless of whether it is held by a Stellar account or a contract address.

Does the Stellar base reserve apply to Soroban contracts?โ–ผ

No. Soroban uses a separate rent-based storage model instead of the classic base reserve system. Contract data entries have a TTL (time-to-live) and require periodic rent extension. This is more flexible and cost-efficient than the fixed reserve model used by classic Stellar ledger entries.

Should I issue my token as a Stellar asset or a Soroban contract token?โ–ผ

Stellar asset (classic) if you need maximum compatibility with exchanges, wallets, and the Stellar DEX. Classic assets are also cheaper for simple transfers.

Soroban contract token if you need custom logic (vesting, governance voting, dynamic supply, access control) beyond what classic assets support. Note: you can also use a hybrid approach โ€” issue a classic asset and interact with it in Soroban via the Stellar Asset Contract (SAC).