Skip to main content

Getting Started with Rust and Solidity

In this tutorial, we'll explore Rust and Solidity, two powerful programming languages. Rust, a systems programming language, is renowned for its safety, concurrency, and performance features, which can be advantageous when building smart contracts. On the other hand, Solidity is a high-level language specifically designed for creating smart contracts on the Ethereum Virtual Machine. This section aims to provide a high-level overview of the similarities and differences between the two languages.

Table of Contents

  1. Solidity Syntax, Data Types, and Basic Constructs
  2. Rust Syntax, Data Types, and Ownership Model
  3. Writing and Interacting With Simple Smart Contracts

Solidity Syntax

Solidity is a programming language designed specifically for creating smart contracts on the Ethereum Virtual Machine (EVM). It has a syntax similar to JavaScript and supports a variety of data types and constructs.

pragma solidity ^0.8.0;

contract HelloWorld {
function sayHello() public pure returns (string memory) {
return "Hello, World!";
}
}

Data Types

Solidity supports various data types, such as:

  • Boolean: bool
  • Integer: int (signed) and uint (unsigned)
  • Address: address
  • String: string
  • Bytes: bytes (dynamic-size) and bytes32 (fixed-size)
  • Arrays: dynamic-size or fixed-size and can be declared with various types.
  • Structs: struct
  • Enums: enum
  • Mapping: mapping

Here are examples of implementations for each data type:

pragma solidity ^0.8.0;

contract DataTypesExample {
// Boolean
bool public isCompleted = false;

// Integer (signed and unsigned)
int256 public signedInteger = -10;
uint256 public unsignedInteger = 10;

// Address
address public userAddress = 0x742d35Cc6634C0532925a3b844Bc454e4438f44e;

// String
string public greeting = "Hello, World!";

// Bytes (dynamic-size and fixed-size)
bytes public dynamicBytes = "hello, solidity";
bytes32 public fixedBytes = "hello, solidity";

// Arrays (dynamic-size and fixed-size)
uint[] public dynamicArray = [1, 2, 3];
uint[5] public fixedArray = [1, 2, 3, 4, 5];

address[] public dynamicAddressArray = [0xd41d1744871f42Bb724D777A2d0Bf53FB43a0040, 0x1f514ae9834aEAF6c2c3eb6D20E27e865F419010];
address[3] public fixedAddressArray = [0xC90cd0D820D6dc447B3cD9545185B046873786A6, 0x401997E856CE51e0D4A8f26ce64952313BEA0E25, 0x221d3b9821f3Cc49B42E7dd487E2a6d1b3ed0E05];

bool[] public dynamicBoolArray = [true, false, true];
bool[2] public fixedBoolArray = [true, false];

// Struct
struct Person {
string name;
uint age;
}
Person public person = Person("Alice", 30);

// Enums
enum Status { Open, Closed, Pending }
Status public currentStatus = Status.Open;
Status public nextStatus = Status.Closed;
Status public previousStatus = Status.Pending;

// Mapping
mapping(address => uint) public balances;

constructor() {
balances[msg.sender] = 100;
}
}

Basic Constructs

Some of the basic constructs in Solidity include:

  1. Variables: Declared with a data type and an identifier.
  2. Functions: Defined with the function keyword.
  3. Modifiers: Used to modify functions' behavior.
  4. Events: Used to log changes in the contract state.
  5. Inheritance: Solidity supports single and multiple inheritance.

We will explore some of these constructs in more detail in the next article, Advanced Solidity Concepts.

Rust Syntax

Rust is a programming language that is well-suited for building smart contracts due to its emphasis on safety, concurrency, and performance. It enforces strict ownership and borrowing rules to prevent data races and other common bugs.

fn main() {
println!("Hello, world!");
}

Data Types

The Soroban Rust SDK supports a variety of Built-In Types which consist of both Primitive and Custom Types, such as:

Primitive Data Types

  • 32-bit Integers: signed (i32) and unsigned (u32)

  • 64-bit Integers: signed (i64) and unsigned (u64)

  • 128-bit Integers: signed (i128) and unsigned (u128)

  • Bool (bool)

  • Bytes, Strings (Bytes, BytesN): byte arrays and strings that can be passed to contracts and stores

  • Vec (Vec): sequential and indexable growable collection type

  • Map (Map): ordered key-value dictionary

  • Address (Address): universal opaque identifier used in contracts

  • String (String): a contiguous growable array type containing u8s and requires an env to be passed in

  • Symbol:

    • (Symbol::new): small efficient strings up to 32 characters in length and requires an env to be passed in

    • (symbol_short!) small efficient strings up to 9 characters in length

    Both are limited to the characters a-zA-Z0-9_ and are encoded into 64-bit integers.

Custom Data Types

  • Structs (with Named Fields): A custom type consisting of named fields stored on the ledger as a map of key-value pairs.
  • Structs (with Unnamed Fields): A custom type consisting of unnamed fields stored on the ledger as a vector of values.
  • Enum (Unit and Tuple Variants): A custom type consisting of unit and tuple variants stored on the ledger as a two-element vector, with the first element being the name of the variant and the second being the value.
  • Enum (Integer Variants): A custom type consisting of integer variants stored on the ledger as the u32 value.

The following are examples of implementations for each data type:

// Integer (signed and unsigned)
let unsigned_32_bit: u32 = 42;
let signed_32_bit: i32 = -42;
let unsigned_64_bit: u64 = 42;
let signed_64_bit: i64 = -42;
let unsigned_128_bit: u128 = 42;
let signed_128_bit: i128 = -42;

// Boolean
let boolean: bool = true;

// String
let msg: &str = "Hello";
String::from_slice(&env, msg)

// Symbols (short and new)
let symbol_short = symbol_short!("Sample"); // up to 9 chars
// env is &Env
let symbol_new = Symbol::new(env, "SampleSymbolExpression");

// Bytes (Bytes and BytesN)
let bytes = Bytes::from_slice(&env, &[1; 32]);
let bytes_n = BytesN::from_array(&env, &[0; 32]);

// Vec
let vec = vec![&env, 0, 1, 2, 3];

// Map
let map = map![&env, (2, 20), (1, 10)];

// Address
let address = Address::new([0u8; 32]);

// Struct (named fields)
pub struct State {
pub count: u32,
pub last_incr: u32,
}

struct Tuple(u32, String);

// Struct (unnamed fields)
pub struct State(pub u32, pub u32);

// Enum (unit and tuple variants)
pub enum Enum {
A,
B(u32),
}

// Enum (integer variants)
pub enum Enum {
A = 0,
B = 1,
}

A Brief Introduction to Modules, Macros, Structs, Traits, and Attribute Macros

In this section, we will provide a concise introduction to some fundamental concepts in Rust: Modules, Macros, Structs, Traits, and Attribute Macros.

These concepts are essential for understanding and writing efficient Rust code, and they will assist you on your journey as a Soroban developer.

1. Modules

Modules in Rust are used to organize and separate code into different namespaces. They enable better code organization, reusability, and encapsulation. To define a module, use the mod keyword followed by a block containing the module's contents.

mod my_module {
pub fn my_function() {
println!("Hello from my_module!");
}
}

2. Macros

Macros in Rust are powerful tools that allow you to do metaprogramming, enabling you to build chunks of reusable code at compile time.

There are two basic types: declarative and procedural macros. The most common is the declarative macro, or plain "macro", which is defined with macro_rules!

macro_rules! my_macro {
() => {
println!("Hello from my_macro!");
};
}

fn main() {
my_macro!();
}

3. Structs

Structs are custom data types in Rust that enable you to bundle data together. They provide a way to define and create more complex data structures.

struct MyStruct {
field1: i32,
field2: String,
}

fn main() {
let my_instance = MyStruct {
field1: 42,
field2: String::from("Hello"),
};
}

4. Traits

Traits in Rust define a shared set of behaviors that types can then either use as-is (default implementations) or implement themselves. They can be thought of as interfaces in other languages. Traits are defined with the trait keyword, and their methods can be implemented for different types using the impl keyword.

trait MyTrait {
fn my_method(&self);
}

struct MyStruct;

impl MyTrait for MyStruct {
fn my_method(&self) {
println!("Hello from MyTrait's my_method!");
}
}

5. Attribute Macros

Attribute macros in Rust are a form of procedural macros that enable you to define custom attributes for various language elements such as functions, structs, and enums. They can modify or generate code based on the annotated items.

// To use an attribute macro, first import it with `use`
use my_attribute_macro::my_attribute;

// Then apply the attribute to an element in your code
#[my_attribute]
fn my_function() {
println!("Hello from my_function!");
}

During your Soroban developer journey, you will frequently encounter the attribute macro #[contractimpl] which exports publicly accessible functions to the Soroban environment.

Functions that are publicly accessible in the implementation are invocable by other contracts, or directly by transactions, when deployed.

#[contractimpl]
impl HelloContract {
pub fn hello(env: Env, to: Symbol) -> Vec<Symbol> {
vec![&env, symbol_short!("Hello"), to]
}
}

Ownership Model

Rust enforces strict ownership rules to manage memory and resources:

  • Each value has a single owner.
  • When the owner goes out of scope, the value is automatically deallocated.
  • Borrowing: Values can be borrowed as immutable or mutable references.
  • Lifetimes: Used to ensure that references remain valid.

Smart Contract Dialect

Contract development in Rust involves certain restrictions due to either unavailable features in the deployment environment or high runtime costs. Thus, the code written for contracts can be seen as a distinct dialect of Rust, focusing on deterministic behavior and minimized code size.

To learn more about Rust's Contract Dialect, check out the Contract Rust Dialect Page.

Writing and Interacting with Simple Smart Contracts

In this section, we'll learn how to write and interact with simple smart contracts in Solidity and Rust.

Writing a Smart Counter in Solidity

Here's an example of a simple Solidity smart contract for a counter:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

contract Counter {
uint256 private _count;

function getCount() public view returns (uint256) {
return _count;
}

function increment() public {
_count += 1;
}
}

Let's break down the layout of the code line by line:

// SPDX-License-Identifier: UNLICENSED

This is a comment that identifies the license for the code. It's not required for the code to run, but it's good practice to include licensing information.

pragma solidity ^0.8.0;

This specifies the version of Solidity that this code was written for. In this case, it's version 0.8.0 or higher.

contract Counter {}

This defines a new Solidity contract called Counter.

uint256 private _count;

This is a private variable called _count of type uint256 (unsigned integer). This variable will be used to store the current value of the counter. It is marked as private, which means it can only be accessed from within the contract.

function getCount() public view returns (uint256) {
return _count;
}

This is a function called getCount() that returns the current value of the counter. The function is marked as public, which means it can be called from outside the contract. The view keyword indicates that this function doesn't modify the state of the contract. The returns keyword specifies the return type of the function.

function increment() public {
_count += 1;
}

This is a function called increment() that increments the counter by 1. It doesn't return anything, but it modifies the state of the contract. Like getCount(), it's marked as public, which means it can be called from both inside and outside the contract.

Interacting with the Solidity Smart Counter

We can interact with the smart contract using the Remix IDE. To do so, follow these steps:

  1. Click the following link to open the Gist in Remix.

  2. Navigate to the Counter.sol file in the file explorer.

Counter

  1. Press Ctrl/Cmd + s to compile the contract.
  2. Navigate to the Deploy & Run Transactions tab and click the Deploy button.

Deploy

The contract should appear under the Deployed Contracts tab:

Deployed

  1. Click the increment button to increment the counter.
  2. Click the getCount button to get the current count.

Increment

Up to this point, we've covered the basics of writing, deploying to a sandbox EVM, and interacting with a simple smart contract using Solidity. In the following section, we will extend our knowledge by learning how to achieve the same outcomes using Rust.

Writing a Smart Counter in Rust

In this section, we'll create a Rust program that simulates the functionality of the Counter smart contract. Here's an example of a simple counter in Rust:

#![no_std]
use soroban_sdk::{contractimpl, log, 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) -> 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 += 1;

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

// Return the count to the caller.
count
}

/// get_count returns the current value of the counter.
pub fn get_count(env: Env) -> u32 {
env.storage().instance().get(&COUNTER).unwrap_or(0)
}
}

This code is an implementation of a smart contract written in Rust using the Soroban Rust SDK, a Rust-based smart contract development toolkit developed by the Stellar Development Foundation (SDF). The Soroban Rust SDK provides a powerful set of tools for writing smart contracts that run on the Soroban Virtual Machine.

Here's a line-by-line explanation of what the code is doing:

#![no_std]

This is a Rust attribute that tells the Rust compiler not to link the Rust standard library. The standard library is extensive, and when deploying Soroban applications, we want to streamline the process as much as possible. By using no_std, we establish a leaner, "barebones" starting point for projects, encompassing only the Rust core and a few other essential components, rather than the full breadth of the standard library.

use soroban_sdk::{contractimpl, log, Env, Symbol};

This code imports necessary items from the Soroban Rust SDK for writing a smart contract. The contractimpl macro is used to implement the smart contract, while the log macro is used for logging messages. The Env struct represents the environment the contract is executing in, and the Symbol type is a small, efficient string type.

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

This creates a new Symbol value with the string "COUNTER". The constant COUNTER is then used as a key to identify the count value stored in the contract storage.

#[contract]
pub struct IncrementContract;

This defines a public struct, IncrementContract, which will contain the implementation of the smart contract.

#[contractimpl]
impl IncrementContract {}

This is a macro that implements the IncrementContract struct as a smart contract.

As previously noted, the #[contractimpl] attribute exports public functions to the Soroban environment. Meaning, these functions become accessible within the implementation and can be invoked by other contracts or directly by transactions upon deployment.

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

This is a public function called increment that takes an Env struct as an argument and returns a u32. Env is the environment the contract is executing in, and u32 is the type of value returned by the function.

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

In this line of code, a mutable variable named count of type unsigned 32-bit integer (u32) is being created. The storage environment is accessed using env.storage(), and the value associated with the key COUNTER is retrieved using the get method. If there is no value set for the key COUNTER, a default value of 0 is used.

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

This logs the current count using the log macro provided by the Soroban Rust SDK.

count += 1;

This increments the count by 1.

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

This saves the updated count back to the contract storage using the set method on the storage object.

count

This returns the updated count to the caller of the function.

pub fn get_count(env: Env) -> u32 {}

This is a public function called get_count that takes an Env struct as an argument and returns a u32. Once more we see the Env which is the environment the contract is executing in, and u32 as the type of the value returned by the function.

env.storage().instance().get(&COUNTER).unwrap_or(0)

This is a repeat of the code we saw earlier, which retrieves the value associated with the key COUNTER from the contract storage. If there is no value set for the key COUNTER, a default value of 0 is used. Finally, the unwrap() method is called to extract the actual value from the Ok wrapper, which is then returned to the caller of the function.

Now that we have written our smart contract, it's time to explore how we can interact with it using the Soroban-CLI, one of many robust Developer Tools available. This powerful command-line tool allows us to interact with the Soroban Virtual Machine from a local machine, providing us with an efficient and flexible way to manage our smart contract.

Interacting with the Rust Smart Counter

To interact with the Rust counter, create a new Rust library using the cargo new command.

cargo new --lib increment

Once the project is created, replace the src/lib.rs file with the code example above.

// Remember to replace your lib.rs file with the code example above.
// This is just a reference to point you in the right direction.
#[contractimpl]
impl IncrementContract {...}

Then, add the following dependencies to the Cargo.toml file:

[package]
name = "increment"
version = "0.1.0"
edition = "2021"

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

[features]
testutils = ["soroban-sdk/testutils"]

[dependencies]
soroban-sdk = "20.0.0"

[dev_dependencies]
soroban-sdk = { version = "20.0.0", features = ["testutils"] }

[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

Note: For a detailed explanation of the Cargo.toml configuration used in this tutorial, check out the Hello World Example.

Next, build the project using the soroban contract build command.

cd increment
soroban contract build

The compiled contract will be located in the target/wasm32-unknown-unknown/release directory.

To interact with the contract, we can use the soroban contract invoke command from the soroban-cli tool. Here's an example of invoking the increment function on a contract with ID 1:

soroban contract invoke \
--wasm target/wasm32-unknown-unknown/release/increment.wasm \
--id 1 \
-- \
increment

The output should be the current value of the counter, which in this case is:

1

You can use the same soroban contract invoke command to increment the counter multiple times.

To get the current value of the counter, you can use the following command:

soroban contract invoke \
--wasm target/wasm32-unknown-unknown/release/increment.wasm \
--id 1 \
-- \
get_count

The output should be the current value of the counter, assuming the counter has been incremented 3 times, the output will be:

3

And that's it! You've learned how to write and interact with simple smart contracts in Solidity and Rust. In the upcoming sections, we'll learn about advanced smart contract concepts, the similarities and differences between Solidity and Rust, and how to develop and deploy smart contracts with Soroban.