
Understand Stellar smart contracts, Soroban’s Rust SDK, storage, authorization, fees, testing, deployment, security, and interview questions.
Stellar is widely known for payments and issued assets, but modern Stellar development also includes programmable smart contracts. The technology historically called Soroban provides the smart-contract environment, Rust SDK, tooling, storage model, authorization framework, and metered execution used to build those applications.
Current official documentation increasingly says “Stellar smart contracts” rather than treating Soroban as a separate network. You will still see soroban-sdk, Soroban authorization, and Soroban-related names throughout the ecosystem. The accurate mental model is: Soroban is Stellar’s smart-contract platform and developer technology; contracts run on Stellar, not on an independent Soroban blockchain.
This guide explains the model in practical terms, builds a small contract, and prepares you for junior-to-mid-level blockchain interviews.
Version-sensitive guide: The commands and code below were verified against the official documentation on August 5, 2026, which lists Stellar CLI 27.1.0 and uses
soroban-sdk = "26"in its getting-started workspace. SDK versions, WebAssembly targets, CLI flags, protocol limits, and event APIs can change. Runstellar version, read the current setup guide, and follow the generated project’s dependency versions.
Stellar is an open-source layer-1 blockchain. It records accounts, assets, balances, operations, and transactions in a sequence of ledgers. Its native asset is the lumen, or XLM, which is used for fees and network requirements.
A smart contract is a deterministic program stored and executed through a blockchain. Users submit signed transactions that invoke contract functions. Every validator must reach the same result, so a contract cannot freely call the public internet or depend on an ordinary system clock, random OS state, or local files.
Stellar smart contracts compile to WebAssembly (Wasm). Developers normally write them in Rust using soroban-sdk, build and interact with them through Stellar CLI, and access the network through Stellar RPC or application SDKs.
Many blockchain roles primarily request Solidity/EVM experience or general Rust experience. Soroban may therefore appear as a “nice-to-have”: valuable evidence that a candidate can learn another execution model, but not always a strict requirement.
It is particularly relevant for teams building:
Knowing Soroban also demonstrates transferable skills: Rust, Wasm, explicit state modeling, authorization trees, resource-aware programming, deterministic testing, and secure financial logic.
A contract exposes public functions. A transaction invokes one or more functions with arguments. The runtime validates authorization, loads declared ledger state, executes deterministic Wasm, meters resources, updates state, and records events if the transaction succeeds.
Important consequences follow:
A practical stack looks like this:
| Layer | Responsibility |
|---|---|
| Application | Wallet or web/mobile UI creates and signs requests |
| SDK / CLI | Builds transactions, simulates resource use, signs, submits, and decodes results |
| Stellar RPC | Gives applications real-time network access and contract simulation/submission APIs |
| Stellar Core / protocol | Validates transactions, executes host functions, reaches consensus, and closes ledgers |
| Soroban host | Runs contract Wasm, exposes storage/auth/crypto/event host functions, and meters resources |
| Ledger | Stores accounts, contract code, contract instances, data entries, assets, and transaction results |
Simulation is an important part of the flow. Before submission, RPC can estimate resources, build the transaction footprint, discover required authorization entries, and calculate a resource fee. Simulation is preparation, not final execution; the network still validates the submitted transaction.
Stellar contracts use a constrained Rust environment and normally begin with #![no_std]. The standard library is not linked into the contract Wasm. Instead, soroban-sdk provides environment-backed types such as Address, Bytes, Map, String, Symbol, and Vec.
The main macros are:
#[contract] marks the contract type.#[contractimpl] exposes public contract functions.#[contracttype] makes a custom type storable and transferable through the contract interface.#[contractevent] defines a typed event in current SDK patterns.Contract inputs are values rather than Rust references. Floating-point numbers are not supported; financial code usually uses scaled integers such as i128 and clearly documents the decimal scale.
An account is a classic Stellar ledger entry identified by a G-address. It holds a sequence number, signers, thresholds, balances, and other settings. A contract has a C-address. Soroban’s Address type gives account and contract addresses a common interface for contract authorization.
An asset can be XLM, a classic Stellar issued asset, or a contract token. The Stellar Asset Contract (SAC) exposes classic assets through a standard contract interface, making them usable in contract calls.
A transaction is a signed envelope containing operations. Contract invocation uses host-function operations and may include authorization entries plus resource declarations. A source account pays fees and supplies the sequence number, but it is not automatically equivalent to every business-level user that a contract should authorize.
A ledger is a closed batch of validated transactions and the resulting state. Ledger sequence and timestamp are available through the contract environment, but developers should understand their protocol semantics before using them for deadlines or financial rules.
Contracts access key-value state through env.storage(). Stellar provides three storage types.
| Storage | Typical use | When TTL expires | Cost/shape |
|---|---|---|---|
| Persistent | Balances, positions, durable user records | Archived and restorable | Most expensive; separate entries |
| Temporary | Approvals, caches, short-lived offers | Deleted permanently | Less expensive; separate entries |
| Instance | Configuration shared by the whole contract | Archived with contract instance | Stored with the instance; size-limited |
TTL means time to live, measured in ledgers. Active entries must have their TTL extended when continued availability matters. Persistent and instance entries can be archived and restored; expired temporary data is gone permanently. Extending or restoring state has a cost.
Do not treat TTL as a background implementation detail. A contract that stores a user balance but never plans for TTL extension or restoration can become unavailable even though its arithmetic is correct.
Contract functions are callable without user authorization unless the function requires it. For a protected action, pass the relevant Address and call:
user.require_auth();
The host verifies that the address authorized the invocation tree. Account addresses can authorize with account signatures; contract addresses can implement custom account authorization through the reserved __check_auth interface.
require_auth_for_args() can bind authorization to a chosen argument set, while require_auth() normally covers the invocation arguments. Always authorize the actual principal whose funds or state will change. Do not assume that checking the transaction source alone implements your business rule.
For cross-contract calls, test the entire authorization tree. The official guide recommends authorizing at the entry point when inner calls act for a user, preventing an authorized inner action from being separated from its intended outer workflow.
Events expose structured facts about successful contract execution for wallets, indexers, analytics, and monitoring. Topics support filtering; data carries the event payload.
Current Protocol 23-era SDK documentation recommends typed event structs with #[contractevent], followed by .publish(&env). This replaced older publishing patterns, so event syntax is especially version-sensitive.
Events are not contract storage. Contracts cannot use historical events as a reliable replacement for state. Applications should also design durable ingestion because RPC event retention is limited.
Stellar transactions pay an inclusion fee. Smart-contract transactions also pay a resource fee based on what they consume and on storage pricing. Metered dimensions include CPU instructions, memory, ledger reads and writes, event/return data, and transaction size.
RPC simulation estimates resource declarations and fees before submission. A contract can still fail if state changes between simulation and execution, authorization is missing, the transaction expires, or network limits are exceeded.
Avoid hard-coding current limits into business logic. Network configuration can change. Instead, keep data compact, minimize unnecessary reads and writes, bound loops by input and protocol limits, and measure representative calls.
This contract lets a player authorize an increase to their own score. It stores each score persistently and publishes a typed event.
Install the current prerequisites from the official setup guide. The August 2026 flow uses Rust 1.84 or newer, the wasm32v1-none target, and Stellar CLI:
rustup update stable
rustup target add wasm32v1-none
stellar version
stellar contract init soroban-score-guide
cd soroban-score-guide
stellar contract init . --name score-contract
The second init adds contracts/score_contract to the generated workspace. Confirm that the root Cargo.toml uses the SDK version generated by your installed CLI.
Replace contracts/score_contract/src/lib.rs with:
#![no_std]
use soroban_sdk::{
contract, contractevent, contractimpl, contracttype, Address, Env,
};
#[contracttype]
#[derive(Clone)]
pub enum DataKey {
Score(Address),
}
#[contractevent(data_format = "single-value")]
pub struct ScoreChanged {
#[topic]
pub player: Address,
pub score: u32,
}
#[contract]
pub struct ScoreContract;
#[contractimpl]
impl ScoreContract {
pub fn add(env: Env, player: Address, points: u32) -> u32 {
player.require_auth();
let key = DataKey::Score(player.clone());
let current: u32 = env.storage().persistent().get(&key).unwrap_or(0);
let updated = current.checked_add(points).expect("score overflow");
env.storage().persistent().set(&key, &updated);
env.storage().persistent().extend_ttl(&key, 1_000, 100_000);
ScoreChanged { player, score: updated }.publish(&env);
updated
}
pub fn get(env: Env, player: Address) -> u32 {
let key = DataKey::Score(player);
env.storage().persistent().get(&key).unwrap_or(0)
}
}
mod test;
DataKey prevents unrelated values from accidentally sharing a storage key. add requires the player’s authorization, uses checked arithmetic, stores the new score, extends the entry’s TTL, emits an event, and returns the new score. get is read-only and intentionally public.
The TTL numbers are tutorial values, not production recommendations. Choose thresholds and extension targets from your access patterns, recovery plan, current ledger-close behavior, and cost measurements.
Replace contracts/score_contract/src/test.rs with:
#![cfg(test)]
use super::*;
use soroban_sdk::{testutils::Address as _, Address, Env};
#[test]
fn player_can_add_to_own_score() {
let env = Env::default();
let contract_id = env.register(ScoreContract, ());
let client = ScoreContractClient::new(&env, &contract_id);
let player = Address::generate(&env);
env.mock_all_auths();
assert_eq!(client.add(&player, &5), 5);
assert_eq!(client.add(&player, &7), 12);
assert_eq!(client.get(&player), 12);
}
Run tests and build Wasm:
cargo test
stellar contract build
ls target/wasm32v1-none/release/*.wasm
mock_all_auths() is convenient for the first functional test, but a production contract also needs tests that assert the exact authorization entries and prove unauthorized calls fail. Add event assertions, boundary tests, overflow tests, TTL tests, and integration tests.
Generate and fund a test identity through Friendbot:
stellar keys generate alice --network testnet --fund
stellar keys address alice
Never publish a secret key or seed phrase. Testnet identities have no production value, but handling them safely builds the correct habit.
Deploy the Wasm and save an alias:
stellar contract deploy \
--wasm target/wasm32v1-none/release/score_contract.wasm \
--source-account alice \
--network testnet \
--alias score_contract
The command returns a C-address. The alias lets later CLI commands refer to that ID.
Invoke the protected function on macOS/Linux:
PLAYER="$(stellar keys address alice)"
stellar contract invoke \
--id score_contract \
--source-account alice \
--network testnet \
-- \
add \
--player "$PLAYER" \
--points 5
Read the score:
stellar contract invoke \
--id score_contract \
--source-account alice \
--network testnet \
-- \
get \
--player "$PLAYER"
PowerShell variable and line-continuation syntax differs. The contract-specific arguments after -- are generated from the contract specification; use this command to inspect them:
stellar contract invoke --id score_contract --network testnet -- --help
Fast unit tests run entirely in Rust with Env::default(). Generated clients call the contract as a normal typed API. The test utilities can mock authorizations, inspect events, control ledger properties, register dependency contracts, and create addresses.
A mature test plan uses several levels:
Common applications include escrow, milestone payments, recurring-payment permissions, token vesting, loyalty points, stablecoin settlement, lending, exchanges, crowdfunding, governance, supply-chain attestations, games, NFT-like assets, and custom account policies.
Soroban is strongest when the application benefits from Stellar’s existing accounts, issued assets, payment rails, and contract programmability together. A contract should add necessary rules, not move every application detail on-chain.
| Topic | Stellar / Soroban | Ethereum / EVM | Solana |
|---|---|---|---|
| Common contract language | Rust with soroban-sdk |
Solidity is |
No approved comments are visible yet. New community replies may wait for moderation.