Skip to main content

Your First Contract

Let’s build a simple smart contract that demonstrates core Soroban SDK concepts. We’ll create an addition contract based on real examples from the SDK test suite.
1

Create the Project

Create a new Rust library:
cargo new --lib my-first-contract
cd my-first-contract
2

Configure Dependencies

Update your Cargo.toml:
Cargo.toml
[package]
name = "my-first-contract"
version = "0.1.0"
edition = "2021"

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

[dependencies]
soroban-sdk = "25.1.1"

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

[profile.release]
opt-level = "z"
overflow-checks = true
debug = 0
strip = "symbols"
debug-assertions = false
panic = "abort"
codegen-units = 1
lto = true
3

Write the Contract

Replace src/lib.rs with this simple addition contract:
src/lib.rs
#![no_std]
use soroban_sdk::{contract, contractimpl};

#[contract]
pub struct Contract;

#[contractimpl]
impl Contract {
    pub fn add(a: u64, b: u64) -> u64 {
        a + b
    }
}

#[cfg(test)]
mod test {
    use soroban_sdk::Env;
    use crate::{Contract, ContractClient};

    #[test]
    fn test_add() {
        let e = Env::default();
        let contract_id = e.register(Contract, ());
        let client = ContractClient::new(&e, &contract_id);

        let x = 10u64;
        let y = 12u64;
        let z = client.add(&x, &y);
        assert!(z == 22);
    }
}
This example is taken from tests/add_u64/src/lib.rs:1 in the Soroban SDK repository.
4

Run the Tests

Test your contract:
cargo test
You should see output indicating the test passed:
running 1 test
test test::test_add ... ok
5

Build the Contract

Build the optimized WASM binary:
cargo build --target wasm32v1-none --release
Your compiled contract will be at:
target/wasm32v1-none/release/my_first_contract.wasm

Understanding the Code

Let’s break down the key components:

Contract Declaration

#[contract]
pub struct Contract;
The #[contract] attribute marks this struct as a Soroban smart contract. The struct itself can be a simple unit struct or contain state.

Contract Implementation

#[contractimpl]
impl Contract {
    pub fn add(a: u64, b: u64) -> u64 {
        a + b
    }
}
The #[contractimpl] attribute designates this implementation block as containing contract methods. Public functions become callable contract entry points.

Testing with Env

let e = Env::default();
let contract_id = e.register(Contract, ());
let client = ContractClient::new(&e, &contract_id);
The SDK automatically generates a ContractClient for testing. This client provides type-safe methods to invoke your contract functions.

More Complex Examples

Contract with Storage

Here’s an example using persistent storage from tests/contract_data/src/lib.rs:1:
#![no_std]
use soroban_sdk::{contract, contractimpl, Env, Symbol};

#[contract]
pub struct Contract;

#[contractimpl]
impl Contract {
    pub fn put(e: Env, key: Symbol, val: Symbol) {
        e.storage().persistent().set(&key, &val)
    }

    pub fn get(e: Env, key: Symbol) -> Option<Symbol> {
        e.storage().persistent().get(&key)
    }

    pub fn del(e: Env, key: Symbol) {
        e.storage().persistent().remove(&key)
    }
}
This contract demonstrates:
  • Storage API: Using e.storage().persistent() for permanent data
  • Key-value operations: set, get, and remove
  • Option types: get returns Option<Symbol> for missing keys

Contract with Constructor

Contracts can have initialization logic using __constructor from tests/constructor/src/lib.rs:1:
#![no_std]
use soroban_sdk::{contract, contractimpl, contracttype, Env};

#[contract]
pub struct Contract;

#[contracttype]
pub enum DataKey {
    Persistent(u32),
    Temp(u32),
    Instance(u32),
}

#[contractimpl]
impl Contract {
    pub fn __constructor(env: Env, init_key: u32, init_value: i64) {
        env.storage()
            .persistent()
            .set(&DataKey::Persistent(init_key), &init_value);
        env.storage()
            .temporary()
            .set(&DataKey::Temp(init_key * 2), &(init_value * 2));
        env.storage()
            .instance()
            .set(&DataKey::Instance(init_key * 3), &(init_value * 3));
    }

    pub fn get_data(env: Env, key: DataKey) -> Option<i64> {
        match key {
            DataKey::Persistent(_) => env.storage().persistent().get(&key),
            DataKey::Temp(_) => env.storage().temporary().get(&key),
            DataKey::Instance(_) => env.storage().instance().get(&key),
        }
    }
}
This demonstrates:
  • Constructor: The __constructor function runs when the contract is deployed
  • Custom types: Using #[contracttype] for enums as storage keys
  • Storage types: Persistent, temporary, and instance storage
Test the constructor by passing initialization arguments to register:
let contract_id = env.register(
    Contract, 
    ContractArgs::__constructor(&100_u32, &1000_i64)
);

Contract with Events

Emit events to track contract activity from tests/events/src/lib.rs:1:
#![no_std]
use soroban_sdk::{contract, contractevent, contractimpl, Address, Env, MuxedAddress};

#[contract]
pub struct Contract;

#[contractevent]
pub struct Transfer {
    #[topic]
    from: Address,
    #[topic]
    to: Address,
    amount: i128,
    to_muxed_id: Option<u64>,
}

#[contractimpl]
impl Contract {
    pub fn transfer(env: Env, from: Address, to: MuxedAddress, amount: i128) {
        Transfer {
            from: from.clone(),
            to: to.address(),
            amount,
            to_muxed_id: to.id(),
        }
        .publish(&env);
    }
}
Key features:
  • Event definition: #[contractevent] defines event structure
  • Topics: #[topic] marks indexed fields for efficient filtering
  • Publishing: .publish(&env) emits the event

Testing Best Practices

Use Env::default()

The default environment simulates a clean contract execution context

Register Contracts

Use env.register() to deploy contracts in the test environment

Generate Test Data

Use Address::generate(&env) for random addresses in tests

Assert Events

Verify events with env.events().all() after contract calls

Common Patterns

Error Handling

use soroban_sdk::contracterror;

#[contracterror]
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
#[repr(u32)]
pub enum Error {
    InvalidAmount = 1,
    InsufficientBalance = 2,
}

Authorization

pub fn transfer(env: Env, from: Address, to: Address, amount: i128) {
    from.require_auth();
    // ... transfer logic
}

Logging (Development)

pub fn debug_info(env: Env, value: u32) {
    env.logs().add().push_u32(value);
}

Build Commands Reference

cargo build --target wasm32v1-none

Next Steps

Now that you’ve built your first contract, explore more advanced topics:

Example Contracts

Explore production-ready contract examples

Developer Guides

Learn advanced patterns and best practices

SDK Documentation

Comprehensive API reference

Soroban CLI

Deploy and interact with contracts