Install Rust, configure your editor, and set up the Stellar CLI. The entire toolchain is free and runs on Windows, macOS, and Linux.
# 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.
rustc --version prints 1.70+rustup target list --installed shows wasm32-unknown-unknownstellar --version prints 21.x+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.
# 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
#![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, ] } }
# 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
#[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.stellar contract init succeededcargo test shows green.wasm file exists in target/wasm32-.../release/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.
# 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
# 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
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.stellar keys address alice shows a public keyC...)stellar contract invoke returned expected outputFollow 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.
#![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() |
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..set() persists data to the ledger.get() retrieves or returns NoneDeploy the increment smart contract on Testnet using the Stellar CLI. Practice the full cycle: build โ optimize โ install WASM โ deploy instance โ invoke โ verify state.
# 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 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.# 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
--releaseBuild 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.
# 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
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);
signTransaction() method pops up Freighter for user approval โ no private keys in your frontend code.client.hello() returns data from contractSoroban 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.
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:
std) is not available โ all contracts use #![no_std]Vec, Map, String which use the Soroban environment's memoryContracts are developed using the Soroban Rust SDK, which consists of a Rust crate and a CLI tool:
olighft-sdk)std โ provides data structures, utility functions, cryptographic hashing, signature verification, persistent storage access, and contract invocation via stable identifiers.olighft)olighft-sdkThe 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:
#![no_std] use olighft_sdk::*; // Env, Address, Vec, Map, contract, contractimpl, โฆ // plus types, math, crypto, auth, storage, invoke
[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 PointsAll math uses i128 checked operations โ overflow returns None instead of panicking. Basis-point scale (รท10 000) is the standard throughout all contracts.
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 VerificationAll operations delegate to the Soroban host via env.crypto() โ zero additional WASM weight:
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 AccessWraps all three Soroban storage tiers with generic, typed helpers. No more env.storage().persistent().get(...) chains:
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 IdentifiersInstead of hard-coding contract addresses, store them under a ContractId enum and resolve at runtime:
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 );
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.olighftA single binary for the entire contract lifecycle. Install with cargo install --path cli from the workspace root:
# 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# 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# 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# 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# 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# 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 ModeSpins 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.
# 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
sandbox-manifest.json with all deployed contract IDs and test account names.The host environment is a set of Rust crates compiled into the SDK CLI tool and stellar-core. It provides:
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.
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.
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.
# 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
Soroban provides three storage tiers, each optimized for different data lifetimes and cost profiles:
// 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);
Soroban provides robust, flexible authorization. Contracts verify signatures from users or other contracts using require_auth(). The auth framework supports:
require_auth() โ Verify the caller signed the transaction (most common)require_auth_for_args() โ Verify the caller authorized specific arguments (for atomic swaps, approvals)stellar contract invoke --simulate-only previews auth requirements before signingDefine 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).
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
#![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 } }
#![no_std] attribute is required โ Soroban contracts run in a WASM sandbox without access to the Rust standard library. Use soroban_sdk types instead.[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"
This compiles your Rust code to target/wasm32-unknown-unknown/release/future_crypto_card.wasm. The optimized binary is typically 10โ50 KB.
soroban contract initrequire_auth()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.
#[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"); } }
Deploy to Stellar's public testnet (Futurenet) for real-world-like testing without risking real funds:
soroban_sdk::testutilsenv.ledger().set() to simulate time passage for staking/compoundingrequire_auth() rejects unauthorized invocationsproptest for random input generationsoroban contract invoke --costcargo testDeploy 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.
opt-level = "z" and lto = trueFirst, install (upload) the WASM bytecode to the network:
# 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...)
# Deploy contract instance from installed WASM soroban contract deploy \ --wasm-hash abc123... \ --network mainnet \ --source deployer-key # Output: Contract ID (e.g., CDLZ...X7KQ)
# 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
soroban contract info --id CDLZ...X7KQ --network mainnet| 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 |
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.
# 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
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; }
Watch the full walkthrough of building your first Soroban smart contract on Stellar.
| 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 |
Rust is installed via rustup โ a version manager that handles the compiler, Cargo, and compilation targets.
Follow the prompts, then restart your terminal or run source $HOME/.cargo/env.
Download and run rustup-init.exe from rustup.rs.
# Add the WebAssembly compilation target (required for contract builds) rustup target add wasm32-unknown-unknown # Keep your toolchain up to date rustup update
The Stellar CLI (stellar, formerly soroban) is the official tool for building, deploying, and invoking contracts.
# 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
soroban-cli to stellar-cli. Both stellar and soroban commands work. All OLIGHFT tooling detects either automatically. This guide uses the stellar name throughout.The project-specific CLI wraps Cargo + Stellar CLI with build, test, inspect, version, deploy, and sandbox commands tailored to the OLIGHFT workspace.
# 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 |
Register Testnet and Mainnet so the CLI knows where to send transactions:
# 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
Identities are local keypairs stored by the CLI. Friendbot funds them with free testnet XLM:
# 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
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.A properly configured editor catches 90% of Soroban errors before you compile:
#[contractimpl] insight{
// 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
}
}
# 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
Run these commands to confirm your environment is fully set up:
# 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
rustc --version prints 1.74+wasm32-unknown-unknown in installed liststellar --version prints 21.x+stellar network ls shows testnetstellar keys address alice returns a keycargo build --target wasm32-unknown-unknown --release succeedscargo test returns greenerror: linker 'cc' not found on Windows
โผ
error[E0463]: can't find crate for 'std' when building for WASM
โผ
#![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
โผ
rustup target add wasm32-unknown-unknown. If you have multiple toolchains, specify: rustup target add wasm32-unknown-unknown --toolchain stable.
#[contract] / #[contractimpl]
โผ
"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
โผ
~/.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.
"account already exists"
โผ
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.
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.
This comparison covers the three primary tokenization models on Stellar to help issuers select the most appropriate model:
| 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) |
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.Smart contracts are immutable once deployed. A single vulnerability can drain all funds with no recourse. Follow these rules as non-negotiable minimums.
require_auth()
Every function that modifies state must authenticate the caller. Without this, anyone can invoke the function on behalf of any address.
// โ 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); }
let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap(); admin.require_auth();Rust release builds disable overflow checks by default, which means i128::MAX + 1 silently wraps to a negative number. Enable them explicitly:
[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.
Public contract functions are the system boundary. Validate every parameter before touching storage:
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); }
Each .set() call costs gas and incurs ongoing rent. Group related data into a single struct to reduce the number of writes:
// โ 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);
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:
An emergency pause mechanism lets you halt the contract if an exploit is discovered:
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.
Always follow the checks โ effects โ interactions (CEI) pattern. Update your own state before calling external contracts:
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, ); }
[profile.release]Soroban charges for CPU instructions, memory, storage reads/writes, and WASM binary size. Every optimization directly reduces user costs.
A smaller binary means lower deploy costs and faster startup. Apply all of these in your release profile:
[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.
Every .get() and .set() call is metered. Read all the data you need into local variables, process, then write back once:
// 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);
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 |
Bump persistent entry lifetimes on every interaction, so active users never lose data to archival:
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);
extend_ttl with a threshold means it only actually extends when the remaining TTL drops below that threshold โ avoiding unnecessary cost on frequent interactions.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.
opt-level = "z", LTO, strip, and codegen-units = 1wasm-opt or olighft build --optimizetemporary() storage tieri128 amounts โ no custom big-int typesShip 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 |
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"); }
mock_all_auths() to verify real enforcementi128::MAX, empty addressesstellar contract invoke --cost within limitsPhase 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:
env.ledger().set() to advance time and test lock expirymock_all_auths() to verify real auth enforcementi128::MAX and values near limitsuse 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:
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:
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:
| 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.
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.
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.
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.
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.
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.).
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.
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.
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.
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.
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.
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).