DEV Community

Cover image for Crafting Stellar Smart Contracts: Learn by creating Smart Contracts in browser
Shweta Kale
Shweta Kale

Posted on

Crafting Stellar Smart Contracts: Learn by creating Smart Contracts in browser

Hey there!๐Ÿ‘‹ Thank you for opening this blog, its my attempt to help everyone understand stellar blockchain as I learn the concepts.
By end of this blog, we'll have built five smart contract, right in your browser. No complicated setup, no downloads, - just you, this blog, and some code. Let's get started!

What we will learn

What's a Smart Contract Anyway?

Imagine a vending machine. You put in a coin, press a button, and out comes your snack. Now, picture that vending machine running on a blockchain, handling not just snacks, but any kind of digital transaction you can think of. That's essentially what Smart Contract does!

In technical terms, a smart contract is a self executing program that lives on blockchain. It automatically makes things happen when certain conditions are met. Here's why this is cool:

  • They're Transparent: Everyone can see the rules (i.e the code and associated transactions), so there are no surprises.
  • They're Trustworthy: Once a smart contract is deployed on blockchain, it can't be changed. What you see is what you get๐Ÿ˜„
  • They're Efficient: They cut out middlemen, making transactions faster and cheaper.
  • They're Permissionless: Anyone can write a smart contract and deploy it.

Now, Stellar has its own flavor of smart contract, and that's what we are going to learn today. Stellar's smart contract platform is called Sorban, and its designed to be simple, fast, and developer-friendly.

The best part? We are going to write this smart contract in Rust on Browser, to do that we are going to use Okashi platform. Don't worry if you are new to Rust - we'll cover everything you need to know.

Rust basics for Stellar Smart Contract

For writing Stellar Smart Contracts we make use of Rust. Rust is a systems programming language that emphasizes safety, concurrency, and performance. Here are some key Rust concepts you'll frequently use in Soroban development:

Structs: Define custom data types, similar to classes in other languages.

struct MyStruct {
    field1: Type1,
    field2: Type2,
}
Enter fullscreen mode Exit fullscreen mode

Impl Blocks: Used to implement methods for Structs.

impl MyStruct {
    fn my_method(&self) {
        // method body
    }
}
Enter fullscreen mode Exit fullscreen mode

Functions: Defined using the fn keyword.

fn greet(env: Env, arg1: Symbol) -> String {
    // Method body
}
Enter fullscreen mode Exit fullscreen mode

Attributes: Metadata applied to code elements, starting with #

#[contract]
struct MyStruct {}
Enter fullscreen mode Exit fullscreen mode

These are just four basic ... related to Rust that we need to know to get started.

Sorban Structure, Data Type and Imports

Soroban is Stellar's smart contract platform. It uses Rust with some specific structures and types that includes:
1. No Standard Library:
Sorban Contracts typically start with #![no_std] to indicate they don't use Rust standard library. We do this to make smart contract smaller and more efficient.

#![no_std]
// Now we're writing a lean, Stellar-ready contract!
Enter fullscreen mode Exit fullscreen mode

2. Importing Sorban SDK:
We need to bring in special Stellar tools. We do this by importing from the Stellar SDK.

use sorban_sdk::{contract, impl, Env};
// Now we've access to Stellar specific features.
Enter fullscreen mode Exit fullscreen mode

3. Contract Structure:

  • Contract are defined as Struct with the #[contract] attribute.
  • Functions are implemented in an impl block with the #[contractimpl].
#[contract]
pub struct ContractName {}

#[contractimpl]
impl ContractName {
}
Enter fullscreen mode Exit fullscreen mode

4. Important Data Types:
a) Env
Env is like smart contract's connection to Stellar World. It let's us access storage, call other imports and more.

use sorban_sdk::{Env};

pub fn my_function(env: Env) {
let current_ledger = env.ledger().sequence();
}
Enter fullscreen mode Exit fullscreen mode

b) Symbol
Symbol is a special, efficient type for short strings. It is often used as keys in storage.

use sorban_sdk::{symbol, Symbol};

let my_symbol: Symbol = symbol!("hello"); 
Enter fullscreen mode Exit fullscreen mode

c) String
This is similar to Rust's standard String, but optimized for Sorban

d) SymbolShort
This is a way to create Symbols at compile-time, which is even more efficient.

use soroban_sdk::{symbol_short};

const MY_KEY: Symbol = symbol_short!("key");
Enter fullscreen mode Exit fullscreen mode

This was just an overview, to get you familiar with the terms and syntax. Don't worry if you didn't understood the keywords and its usage. By end of this tutorial you definitely will.

Our First Smart Contract

Now that we have basic understanding, let's write our "Hello World" contract step by step.

Step 1: The Contract Structure -

#![no_std]
use soroban_sdk::{contract, contractimpl, String, vec, Env, Vec};

#[contract]
pub struct HelloContract;

#[contractimpl]
impl HelloContract {
    // We'll add our functions here
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • We start with #![no_std] to indicate we're not using the standard library.
  • We import necessary items from soroban_sdk.
  • We define our contract struct HelloContract and mark it with #[contract].
  • We start an implementation block for HelloContract and mark it with #[contractimpl].

Its all same thing that we learned above, we have just put the blocks together.

Step 2: Implementing the method

The code we saw above was just structure, without including any public function in it the code does not have any meaning even if we deploy it.

 pub fn greeting_from(env: Env, from: String) -> Vec<String> {
    vec![&env, String::from_str(&env, "Hello, I'm "), from]
 }
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • We created a public function called greeting_from which has two arguments - Env and from.
  • -> Vec<String> signifies that we are returning a vector of type String, with text "Hello, I'm " and input user entered.

Putting it together we will have:

#![no_std]
use soroban_sdk::{contract, contractimpl, String, vec, Env, Vec};

#[contract]
pub struct HelloContract;

#[contractimpl]
impl HelloContract {
    pub fn greeting_from(env: Env, from: String) -> Vec<String> {
      vec![&env, String::from_str(&env, "Hello, I'm "), from]
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Running Smart Contract on Okashi

Okashi is a place where we can experiment with our stellar smart contract in browser, so no need to setup any code locally.
It's like a digital workspace with three main sections:

  • Code Window: Where we will write our smart contract.
  • Contract Window: Calling the functions from our smart contract.
  • Console Window: Show us result of our experiment, i.e output.

Okashi.dev

To try the code we wrote
a) Go to Okashi.dev playground.
b) Paste the code in code section.
c) Click on Compile.
d) In the contract window you should see a method greeting_from, click on it and enter input in modal.
e) You will see the output in the Console window.

You can also see the output in this link.
Congratulations!๐ŸŽ‰ You've just written your first Soroban smart contract in Rust.

Second Smart Contract - Creating multiple methods in one contract.

For our second smart contract we will work on creating more that one method in contract and learn how to access it.

Let's try creating a new smart contract TitleContract which has two methods

  • set_title: Get's input from user and save the title.
  • get_title: Returns input user entered.

Step 1: Basic structure

#![no_std]
use soroban_sdk::{contract, contractimpl, symbol_short, Env, Symbol, String};


#[contract]
pub struct TitleContract;

#[contractimpl]
impl TitleContract {
    pub fn set_title(env: Env, title: String) {
    }

    pub fn get_title(env: Env) -> String {
    }
}

Enter fullscreen mode Exit fullscreen mode

As mentioned in task we have two methods get_title and set_title, the structure is similar to first contract.

Step 2: Implementing the methods

Before we implement the method we need to understand basics of data storage in stellar smart contract.

Smart Contract Storage in Stellar:
Smart contract storage in Stellar is designed to accommodate a wide range of uses affordably. It's primarily used for storing data related to the state and functionality of smart contracts deployed on the Soroban platform.

  • To store the data we need to have a key using which we can identify the data stored to use it.
  • Data in storage can be set using set method and retrieved using get method.

In Stellar we have three type of storage
a) Temporary Storage - Temporary storage is ideal for data that is only needed for a short period and can be discarded afterwards without consequences. It's the cheapest option but has a limited lifespan.

Example: A contract that stores the recent price of BTC against USD.

Can be accessed using -

// save the VALUE in temporary storage using set with key as KEY
env.storage().temporary().set(KEY, VALUE)

// get the VALUE from temporary storage using key, KEY
env.storage().temporary().get(KEY)
Enter fullscreen mode Exit fullscreen mode

b) Persistent Storage - Persistent storage is used for long-term data storage. When the TTL (Time to Live) reaches zero, the data is archived rather than deleted and can be restored when needed.

Example: A loyalty points system where users accumulate points.

Can be accessed using -

// save the VALUE in persistent storage using set with key as KEY
env.storage().persistent().set(KEY, VALUE)

// get the VALUE from persistent storage using key, KEY
env.storage().persistent().get(KEY)
Enter fullscreen mode Exit fullscreen mode

c) Instance Storage - Instance storage is a type of storage that is tied directly to the contract instance itself.

Example: A voting system where each contract instance represents a unique vote.

Can be accessed using -

// save the VALUE in instance storage using set with key as KEY
env.storage().instance().set(KEY, VALUE)

// get the VALUE from instance storage using key, KEY
env.storage().instance().get(KEY)
Enter fullscreen mode Exit fullscreen mode

We will understand this more in next examples, For this example we will use instance storage.

Creating Key for Storage in our contract

We will create key, TITLE to access instance storage

const TITLE: Symbol = symbol_short!("title");
Enter fullscreen mode Exit fullscreen mode

Saving and Retrieving Title data

// set_title
env.storage().instance().set(&TITLE, value);

// get_title
env.storage().instance().get(&TITLE).unwrap_or(String::from_str(&env, "No Title Set"));
Enter fullscreen mode Exit fullscreen mode

In above code we are setting instance storage with key TITLE
We are retrieving it using get() and if no value is present we return a default title "No Title Set" using unwrap_or.

Step 3: Putting it together

#![no_std]
use soroban_sdk::{contract, contractimpl, symbol_short, Env, Symbol, String};

const TITLE: Symbol = symbol_short!("title");

#[contract]
pub struct TitleContract;

#[contractimpl]
impl TitleContract {
    pub fn set_title(env: Env, title: String) {
        env.storage().instance().set(&TITLE, &title);
    }

    pub fn get_title(env: Env) -> String {
        env.storage().instance().get(&TITLE).unwrap_or(String::from_str(&env, "No title set"))
    }
}
Enter fullscreen mode Exit fullscreen mode
  • In above code set_title accepts argument called title and sets it with instance storage.
  • In get_title we retrieve the key and return the value present, if value is not present we return default value.

Step 4: Testing code in Okashi.dev

Paste the code in Okashi playground and click on Compile.
The contract window should have two methods.

  • If you click on get_title you should see default value in console.
  • Now set title by clicking on set_title and click on get_title. You should see the title you entered.

Yep, that's it. We have our second smart contract working fine. I know you might have some doubt about storage, different types available and why did we select instance storage. I've been there too, it would be much clearer before we end this blog, so for now let us keep basic in mind - Smart contract have three type of storage - In our TitleContract, we used instance as we only want to use it till the contract instance is active.

You can try the code here: https://okashi.dev/playground/btpyseknmnjmhfedbrxilgirhnvo

Congratulations!๐ŸŽ‰ We have worked on creating smart contract with two methods and learned about different types of smart contract storage and used instance storage.

Our Third Smart Contract

Let us create a new smart contract that use instance storage to increment counter.

You need to create a new Smart Contract - IncrementCounterContract. This contract should have one public method called as increment and a key which you will use for saving and retrieving data from instance storage

Step 1: Coding the contract

This part is similar to second contract we wrote, so let's create contract, it should be a good revision for us.

#![no_std]
use soroban_sdk::{contract, contractimpl, Env, symbol_short, Symbol};
const COUNTER: Symbol = symbol_short!("COUNTER");

#[contract]
pub struct IncrementContract;

#[contractimpl]
impl IncrementContract {

    pub fn increment(env: Env, value: u32) -> u32 {

        let mut count: u32 = env.storage().instance().get(&COUNTER).unwrap_or(0);

        // Increment the count.
        count += value;

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

        // Return the count to the caller.
        count
    }
}
Enter fullscreen mode Exit fullscreen mode

In above code we have method increment, which return integer value, i.e count.

Step 2 Understanding contract instance and TTL

Well, the contract we wrote above is fine but with instance storage, the contract storage and its instance have default lifetime determined by network's minimum time to live(TTL) for contract's instance.

To handle that we need to extend the time to live (TTL) for the instance storage. Before doing that let's first understand technical terms I mentioned above

Contract Instance:

A contract instance refers to a deployed smart contract on the Stellar network. When you deploy a contract, you're creating an instance of that contract on the blockchain. This instance has its own unique identifier and can maintain its own state.

Example:
Let's say you deploy a token contract. This deployed contract becomes an instance, and it can have its own storage, including things like:

  • The token's name
  • Total supply
  • Administrator address

Time To Live (TTL)

Contract instances on Stellar have an archival TTL (Time-To-Live) that determines how long they remain active. This TTL is tied to the contract instance itself.

A contract instance becomes inactive when its TTL expires. However, it's important to note that the TTL can be extended to keep the contract instance active for a longer period.

We are using Instance Storage. As we discussed above anything stored in instance storage has an archival TTL that is tied to the contract instance itself. So, if a contract is live and available, the instance storage is guaranteed to be so, too.

Now, we will learn how to prevent instance storage from expiring and extend its TTL

Extending Instance Storage TTL

env.storage().instance().extend_ttl(50, 100);
Enter fullscreen mode Exit fullscreen mode

This line of code is used to extend the Time-To-Live (TTL) of the contract's instance storage. Here's how it works:

  1. It extends the TTL by 50 ledgers.
  2. It sets a minimum TTL of 100 ledgers.

What this means:

  1. If the current TTL is less than 100 ledgers, it will be set to 100.
  2. If the current TTL is already 100 or more, it will be extended by 50 ledgers.

This operation ensures that the contract instance and its associated instance storage remain active and accessible for at least the specified number of ledgers.

To summarize:

  1. Contract instances become inactive when their TTL expires.
  2. The TTL is tied to the contract instance and its associated data.
  3. Developers can extend the TTL to keep contract instances active for longer periods.

By managing the TTL, developers can control how long their contract instances remain active on the Stellar blockchain.

Let's add relevant code in our contract

#![no_std]
use soroban_sdk::{contract, contractimpl, log, symbol_short, Env, Symbol};


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

#[contract]
pub struct IncrementContract;

#[contractimpl]
impl IncrementContract {
    /// Increment increments an internal counter, and returns the value.
    pub fn increment(env: Env, value: u32) -> u32 {
        // Get the current count.
        let mut count: u32 = env.storage().instance().get(&COUNTER).unwrap_or(0); // If no value set, assume 0.
        log!(&env, "count: {}", count);

        // Increment the count.
        count += value;

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

        // The contract instance will be bumped to have a lifetime of at least 100 ledgers if the current expiration lifetime at most 50.
        // If the lifetime is already more than 100 ledgers, this is a no-op. Otherwise,
        // the lifetime is extended to 100 ledgers. This lifetime bump includes the contract
        // instance itself and all entries in storage().instance(), i.e, COUNTER.
        env.storage().instance().extend_ttl(50, 100);

        // Return the count to the caller.
        count
    }
}

Enter fullscreen mode Exit fullscreen mode

You can access the code in this playground -
https://okashi.dev/playground/bcjohutecdojquhnafveonzkttxk

Step 3: Compiling the contract

Simply copy paste the code in Okashi playground, and click on Compile.
Vola!! You can test your increment counter and it should work just fine.

Try reloading the playground, and run the method again. Counter starts from zero. Why? because we did not deploy the contract and its running in a mock environment.

Well we did compile this and all our previous smart contract. Does that mean its available on blockchain?
Not really, To make it available on blockchain we will deploy it on testnet. Let's do that in next step.

Step 4: Deploying Smart Contract on Okashi

Did we not deploy our contract earlier?
No, Okashi allows you to run and test your contract locally without deploying it to the actual network. This is incredibly useful for development and testing purposes.

What does it mean that we deploy our contract?
Deploying a contract means uploading your smart contract code to the blockchain and making it available for interaction. It's essentially the process of putting your contract "live" on the network. Here's what happens when you deploy a contract:

  1. The contract's compiled WebAssembly (Wasm) code is uploaded to the network.
  2. A unique contract ID is generated for your deployed contract.
  3. The contract becomes accessible and can be interacted with by users or other contracts.

Deployment in Sorban
In Sorban deployment is typically a two-step process:

  1. Install: This install the contract's WASM bytecode to the network.
  2. Deploy: This creates instance of contract with unique contract ID.

Okashi deploy

To deploy smart contract on testnet simply click on settings icon in left panel and select testnet as shown in above image.

Deploy smart contract

Hurray!!! Now we have completed 3 contracts, and learned to deploy it on the network so that even other users can interact with it.
All this in browser. Isn't that great??

Our Fourth Smart Contract - User Authentication

Picture this, instead of increment counter you have a balance book where balance increase and decrease. We only have one balance so anyone who run this method the balance will be affected. Huhh, so everyone has a common balance? That's not correct right?

To handle this we will use user authentication to our increment counter code.

In stellar we can use user.require_auth() to make sure user has authenticated the transaction.

Step 1: Adding require_auth in code

a) Update imports from sdk to import Address.
b) Accept one more argument from increment method called user of type Address.
c) Add user.require_auth() before we perform any computation or updates.

 pub fn increment(env: Env, user: Address, value: u32) -> u32 {
user.require_auth();
// other code remain same
        }
Enter fullscreen mode Exit fullscreen mode

You can access code here - https://okashi.dev/playground/buscouoocaofizhsoeddwufiiwke

What does this mean? It means that whenever increment is called, user is authenticating to make the change.

d) Compile contract and run the increment method. You will notice that you have a new field as input -> some user options.
Select one user, Alex and click on call.

Okashi compile

So, our code complied successfully and we have output. Are we done?
No not at all, we just implemented user authentication. All the data is stored in same key. If you try calling increment again, this time select Bart. The value is updated with count Alex had. We don't want this to happen!!

To handle this we need unique key for each user. Let's do that in second step.

Step 2: Adding unique key for each user

Instead of having a same key, i.e COUNTER. Let's use User's address as a key to store the increments. We can do that by replacing current key with &user.clone()

.clone() is called on user to create a new copy of the address. This is necessary if user is not owned by this part of the code or if it needs to be used elsewhere after this operation.

        env.storage().instance().set(&user.clone(), &count);
Enter fullscreen mode Exit fullscreen mode

This is complete code for your reference

#![no_std]
use soroban_sdk::{contract, contractimpl, log, Env, Address};

#[contract]
pub struct IncrementContract;

#[contractimpl]
impl IncrementContract {
    /// Increment increments an internal counter, and returns the value.
    pub fn increment(env: Env, user: Address, value: u32) -> u32 {
user.require_auth();

        // Get the current count.
        let mut count: u32 = env.storage().instance().get(&user.clone()).unwrap_or(0); // If no value set, assume 0.
        log!(&env, "count: {}", count);

        // Increment the count.
        count += value;

        // Save the count.
        env.storage().instance().set(&user.clone(), &count);


        // Return the count to the caller.
        count
    }
}

Enter fullscreen mode Exit fullscreen mode

Step 3: Compile and deploy the code

You can use Okashi to test the code and check if it works fine.
If you call increment from one user, Alex, it should not affect value stored in other user.
You can access the code here- https://okashi.dev/playground/asuxzxwasubvfypwzyqhaqyfgywz

Our Fifth Smart Contract

This is last contract we are going to discuss for this blog.
Till now we

  • understood smart contract structure
  • Learned three type of smart contract storage
  • used instance storage to implement increment counter and title contract
  • Learned about Contract instance and TTL.
  • Learned how to use authentication in contract
  • Deployed our contract on Testnet

That's a lot we covered in this blog, but I'm sure you are still confused between instance storage and persistent storage.
This is our chance to understand proper difference between these two.

In our fourth smart contract we used user authentication, so many users can use same contract and each will have their separate counter as we have unique key. Everything seems to be correct right?
Not exactly, this works but in this case we can use persistent storage as
instance storage wouldn't work well for this use case:

a) Limited Storage: Instance storage has a limited amount of space. With a growing number of users, you could quickly run out of storage.

b) Performance Impact: Every piece of data stored in instance() storage is retrieved from the ledger every time the contract is invoked. Even if the invoked function does not interact with any ledger data at all.
This means that as your user base grows, contract invocations would become increasingly expensive and slower, as all counts would be loaded each time.

Persistent storage is ideal for this scenario because it allows for efficient storage and retrieval of individual user data, scales well with a growing user base, and ensures data availability regardless of the contract instance's active status.

Storage type difference

Converting our code to use persistent storage is easy, you just need to replace instance storage with persistent storage and its done.
Here is complete code for your reference -

#![no_std]
use soroban_sdk::{contract, contractimpl, log, symbol_short, Env, Symbol, Address};


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

#[contract]
pub struct IncrementContract;

#[contractimpl]
impl IncrementContract {
    /// Increment increments an internal counter, and returns the value.
    pub fn increment(env: Env, user: Address, value: u32) -> u32 {
user.require_auth();

        // Get the current count.
        let mut count: u32 = env.storage().persistent().get(&COUNTER).unwrap_or(0); // If no value set, assume 0.
        log!(&env, "count: {}", count);

        // Increment the count.
        count += value;

        // Save the count.
        env.storage().persistent().set(&COUNTER, &count);


        // Return the count to the caller.
        count
    }
}
Enter fullscreen mode Exit fullscreen mode

You can try the code here- https://okashi.dev/playground/caavrhvfctirmkryboblgxkwxshl

Conclusion

That was a longest blog I have ever written, and I really learned a lot of stuff while writing this blog.

In this blog we learned:

  • The basics of smart contracts and their importance in blockchain technology
  • Fundamental Rust concepts used in Soroban development
  • How to structure Soroban smart contracts
  • Different types of storage in Stellar smart contracts (temporary, persistent, instance)
  • How to implement multiple methods within a single contract
  • The concept of contract instances and Time To Live (TTL)
  • How to use user authentication in smart contracts
  • The process of deploying smart contracts on the Stellar testnet
  • When to use persistent vs instance storage for scalability

Contract Recap

With this knowledge, we can start working on more complex Stellar smart contracts, such as:

  • Token systems
  • Decentralized exchanges
  • Voting systems
  • Multi-signature wallets
  • Cross-chain atomic swaps

The examples we explored were adapted from https://github.com/stellar/soroban-examples, which is an excellent resource for further learning and experimentation.

This tutorial aimed to provide a solid foundation for beginners to start their journey into Stellar smart contract development. Would love to hear your feedback and thoughts!

Remember, the best way to learn is by doing. So don't hesitate to experiment with the concepts we've covered, modify the example contracts, and create your own unique smart contracts on the Stellar network. Happy coding!

Top comments (0)