Skip to main content

3. Storing Data

Now that we've built a basic Hello World example to see the rough structure of Soroban contracts, we'll write a simple contract that stores and retrieves data. This will help you see the basics of Soroban's storage system. We'll also organize the two contracts as one combined project using a Cargo Workspace, which is a common pattern for Soroban projects.

This is going to follow along with the increment example, which has a single function that increments an internal counter and returns the value. If you want to see a working example, try it in GitPod.

This tutorial assumes that you've already completed the previous steps in Getting Started: Setup, Hello World, and Deploy to Testnet.

Setting up a multi-contract project

Many Soroban projects need more than one contract. Cargo makes this easy with workspaces, though it doesn't yet give a way to initialize a new project as a workspace (see #8365). Let's set it up manually.

Rather than just a hello-soroban folder, we want a new soroban-tutorial folder with a contracts folder inside, into which we'll move the existing hello-soroban project. As a diff, we want this:

-hello-soroban
+soroban-tutorial/contracts/hello-soroban

So change into the parent directory of hello-soroban and:

mkdir -p soroban-tutorial/contracts
mv hello-soroban soroban-tutorial/contracts
cd soroban-tutorial

You're going to want some Rust and Cargo stuff in different spots. From the new project root, run:

rm contracts/hello-soroban/Cargo.lock
mv contracts/hello-soroban/target .
mv contracts/hello-soroban/.soroban .
cp contracts/hello-soroban/Cargo.toml .

Note that we copied the Cargo.toml file. That's because we're going to need some of it in the root and some of it in the subdirectory.

In the root Cargo.toml:

  • remove the [package], [lib], [features], and [dev_dependencies] sections
  • keep the [profile.release*] stuff
  • replace the line [dependencies] with [workspace.dependencies]
  • add a [workspace] section (see below for specific values)

In the contract-specific Cargo.toml:

  • remove the [profile.release*] stuff
  • set the dependency versions to use the workspace versions (see example below)

It all ends up looking like this:

[workspace]
resolver = "2"
members = [
"contracts/*",
]

[workspace.dependencies]
soroban-sdk = "20.0.0"

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

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

Now make sure everything works:

soroban contract build

Everything should build.

cargo test

All tests should pass.

Code

Rather than initializing the new contract with cargo new, let's copy the hello-soroban project:

cp -r contracts/hello-soroban contracts/incrementor

You'll need to update the Cargo.toml file to reflect the correct name:

contracts/incrementor/Cargo.toml
 [package]
-name = "hello-soroban"
+name = "incrementor"
version = "0.1.0"
edition = "2021"

And now in contracts/incrementor/src/lib.rs, we'll replace the contents with the following:

contracts/incrementor/src/lib.rs
#![no_std]
use soroban_sdk::{contract, contractimpl, log, symbol_short, Env, Symbol};

const COUNTER: Symbol = symbol_short!("COUNTER");

#[contract]
pub struct IncrementorContract;

#[contractimpl]
impl IncrementorContract {
/// Increment an internal counter; return the new value.
pub fn increment(env: Env) -> u32 {
let mut count: u32 = env.storage().instance().get(&COUNTER).unwrap_or(0);

count += 1;

log!(&env, "count: {}", count);

env.storage().instance().set(&COUNTER, &count);

env.storage().instance().extend_ttl(100, 100);

count
}
}

#[cfg(test)]
mod test;

Make sure it builds:

soroban contract build

Check that it built:

ls target/wasm32-unknown-unknown/release/*.wasm

You should see both hello_soroban.wasm and incrementor.wasm.

How it Works

Follow along in your contracts/incrementor/src/lib.rs file.

Contract Data Keys

Contract data is associated with a key. The key can be used at a later time to look up the value.

Symbol is a short (up to 32 characters long) string type with limited character space (only a-zA-z0-9_ characters are allowed). Identifiers like contract function names and contract data keys are represented by Symbols.

The symbol_short!() macro is a convenient way to pre-compute short symbols up to 9 characters in length at compile time using Symbol::short. It generates a compile-time constant that adheres to the valid character set of letters (a-zA-Z), numbers (0-9), and underscores (_). If a symbol exceeds the 9-character limit, Symbol::new should be utilized for creating symbols at runtime.

const COUNTER: Symbol = symbol_short!("COUNTER");

Contract Data Access

The Env.storage() function is used to access and update contract data. The executing contract is the only contract that can query or modify contract data that it has stored. The data stored is viewable on ledger anywhere the ledger is viewable, but contracts executing within the Soroban environment are restricted to their own data.

The get() function gets the current value associated with the counter key.

let mut count: u32 = env
.storage()
.instance()
.get(&COUNTER)
.unwrap_or(0); // If no value set, assume 0.

If no value is currently stored, the value given to unwrap_or(...) is returned instead.

Values stored as contract data and retrieved are transmitted from the environment and expanded into the type specified. In this case a u32. If the value can be expanded, the type returned will be a u32. Otherwise, if a developer caused it to be some other type, a panic would occur at the unwrap.

The set() function stores the new count value against the key, replacing the existing value.

env.storage()
.instance()
.set(&COUNTER, &count);

Managing Contract Data TTLs with extend_ttl()

All contract data has a Time To Live (TTL), measured in ledgers, that must be periodically extended. If an entry's TTL is not periodically extended, the entry will eventually become "archived". You can learn more about this in the State Archival document.

For now, it's worth knowing that there are three kinds of storage: Persistent, Temporary, and Instance. This contract only uses Instance storage: env.storage().instance(). Every time the counter is incremented, this storage's TTL gets extended by 100 ledgers, or about 500 seconds.

Tests

Open the contracts/increment/src/test.rs file and replace the contents with:

contracts/incrementor/src/test.rs
use crate::{IncrementorContract, IncrementorContractClient};
use soroban_sdk::Env;

#[test]
fn increment() {
let env = Env::default();
let contract_id = env.register_contract(None, IncrementorContract);
let client = IncrementorContractClient::new(&env, &contract_id);

assert_eq!(client.increment(), 1);
assert_eq!(client.increment(), 2);
assert_eq!(client.increment(), 3);
}

This uses the same concepts described in the Hello World example.

Make sure it passes:

cargo test

You'll see that this runs tests for the whole workspace; both the Hello World contract and the new Incrementor.

If you want to see the output of the log! call, run the tests with --nocapture:

cargo test -- --nocapture

You should see the output:

running 1 test
count: U32(0)
count: U32(1)
count: U32(2)
test test::incrementor ... ok

Take it further

Can you figure out how to add get_current_value function to the contract? What about decrement or reset functions?

Commit your changes

Looking at your git diff will be interesting now. It's probably kind of noisy if you just run git status or git diff right away, but once you git add ., git will understand the renames (aka "moves") of all the old hello-soroban files better.

Go ahead and commit it.

git commit -m "add incrementor contract"

Summary

In this section, we added a new contract to this project, reorganizing the project as a multi-contract project using Cargo Workspaces. The new contract made use of Soroban's storage capabilities to store and retrieve data. We also learned about the different kinds of storage and how to manage their TTLs.

Next we'll learn a bit more about deploying contracts to Soroban's Testnet network and interact with our incrementor contract using the CLI.