Basic Rust for Solidity Developers
This tutorial goes over the most commonly used syntax in Solidity and demonstrates the equivalent in Rust.
If you want a high level overview of the differences of Rust vs Solidity please see the linked tutorial. This tutorial assumes you already know Solidity, so please see our free Solidity tutorial if you are unfamiliar with Solidity.
Create a new Solana Anchor project called tryrust
and set up the environment.
Conditional statements
We can say there are 2 ways developers can control the flow of execution based on a specific condition in Solidity:
- If-Else Statements
- Ternary operator
Now let’s see the above in Solidity, and their translations to Solana.
If-Else Statements
In Solidity:
function ageChecker(uint256 age) public pure returns (string memory) {
if (age >= 18) {
return "You are 18 years old or above";
} else {
return "You are below 18 years old";
}
}
In Solana, add a new function called age_checker
in lib.rs:
pub fn age_checker(ctx: Context<Initialize>, age: u64) -> Result<()> {
if age >= 18 {
msg!("You are 18 years old or above");
} else {
msg!("You are below 18 years old");
}
Ok(())
}
Note that the condition age >= 18
does not have parenthesis — those are optional for if statements.
To test, add another it
block in ./tests/tryrust.ts
:
it("Age checker", async () => {
// Add your test here.
const tx = await program.methods.ageChecker(new anchor.BN(35)).rpc();
console.log("Your transaction signature", tx);
});
We should have the following logs after running the test:
Transaction executed in slot 77791:
Signature: 2Av18ej2YjkRhzybbccPpwEtkw73VcBpDPZgC9iKrmf6mvwbqjA517garhrntWxKAM1ULL2eAv5vDWJ3SjnFZq6j
Status: Ok
Log Messages:
Program 53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX invoke [1]
Program log: Instruction: AgeChecker
Program log: You are 18 years old or above
Program 53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX consumed 440 of 200000 compute units
Program 53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX success
Ternary operator
Assigning if-else statement to a variable in Solidity:
function ageChecker(uint256 age) public pure returns (bool a) {
a = age % 2 == 0 ? true : false;
}
To do this in Solana, we basically just assign an if-else statement to a variable. The Solana program below is the same as the above:
pub fn age_checker(ctx: Context<Initialize>, age: u64) -> Result<()> {
let result = if age >= 18 {"You are 18 years old or above"} else { "You are below 18 years old" };
msg!("{:?}", result);
Ok(())
}
Note that in the Ternary Operator example in Rust, the if/else block ends with a semi colon as this is being assigned to a variable.
Also notice that the inner values do not have a semicolon at the end because it is being returned as the return value to the variable, similar to how you don’t put a semicolon after Ok(())
as it’s an expression and not a statement.
Program logs out true if age is even, else false:
Transaction executed in slot 102358:
Signature: 2zohZKhY56rLb7myFs8kabdwULJALENyvyFS5LC6yLM264BnkwsThMnotHNAssJbQEzQpmK4yd3ozs3zhG3GH1Gx
Status: Ok
Log Messages:
Program 53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX invoke [1]
Program log: Instruction: AgeChecker
Program log: true
Program 53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX consumed 792 of 200000 compute units
Program 53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX success
Rust has one more powerful control flow construct called match. Let’s see an example of using match below:
pub fn age_checker(ctx: Context<Initialize>, age: u64) -> Result<()> {
match age {
1 => {
// Code block executed if age equals 1
msg!("The age is 1");
},
2 | 3 => {
// Code block executed if age equals 2 or 3
msg!("The age is either 2 or 3");
},
4..=6 => {
// Code block executed if age is in the
// range 4 to 6 (inclusive)
msg!("The age is between 4 and 6");
},
_ => {
// Code block executed for any other age
msg!("The age is something else");
}
}
Ok(())
}
For Loops
As we know, for loop allows looping over ranges, collections, and other iterable objects and it is written in Solidity like so:
function loopOverSmth() public {
for (uint256 i=0; i < 10; i++) {
// do something...
}
}
This is the equivalent in Solana (Rust):
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
for i in 0..10 {
// do something...
}
Ok(())
}
Yes, it’s that simple, but how do we iterate over a range with a custom step? Here is the intended behavior in Solidity:
function loopOverSmth() public {
for (uint256 i=0; i < 10; i+=2) {
// do something...
// Increment i by 2
}
}
Here is the equivalent in Solana using step_by
:
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
for i in (0..10).step_by(2) {
// do something...
msg!("{}", i);
}
Ok(())
}
Running the test, we should have the following logs:
Transaction executed in slot 126442:
Signature: 3BSPA11TZVSbF8krjMnge1fgwNsL9odknD2twAsDeYEF39AzaJy1c5TmFCt6LEzLtvWnjzx7VyFKJ4VT1KQBpiwm
Status: Ok
Log Messages:
Program 53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX invoke [1]
Program log: Instruction: Initialize
Program log: 0
Program log: 2
Program log: 4
Program log: 6
Program log: 8
Program 53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX consumed 2830 of 200000 compute units
Program 53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX success
Arrays and Vectors
Rust differs from Solidity in terms of array support. While Solidity has native support for both fixed and dynamic arrays, Rust only has built-in support for fixed arrays. If you want a dynamic-length list, use a vector.
Now, let’s look at some examples that demonstrate how to declare and initialize both fixed and dynamic arrays.
Fixed array
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
// Declare an array of u32 with a fixed size of 5
let my_array: [u32; 5] = [10, 20, 30, 40, 50];
// Accessing elements of the array
let first_element = my_array[0];
let third_element = my_array[2];
// Declare a mutable array of u32 with a fixed size of 3
let mut mutable_array: [u32; 3] = [100, 200, 300];
// Change the second element from 200 to 250
mutable_array[1] = 250;
// Rest of your program's logic
Ok(())
}
Dynamic array
A method to simulate a dynamic array in Solana involves utilizing a Vec
(Vector) from the Rust standard library. Here is an example below:
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
// Declare a dynamic array-like structure using Vec
let mut dynamic_array: Vec<u32> = Vec::new();
// Add elements to the dynamic array
dynamic_array.push(10);
dynamic_array.push(20);
dynamic_array.push(30);
// Accessing elements of the dynamic array
let first_element = dynamic_array[0];
let third_element = dynamic_array[2];
// Rest of your program's logic
msg!("Third element = {}", third_element);
Ok(())
}
The dynamic_array
variable must be declared as mutable (mut
) to allow mutating (push, pop, override at an index, etc).
Program should log this after running the test:
Transaction executed in slot 195373:
Signature: 4113irrcBsFbNaiZia5c84yfJpS4Hn4H1QawfUSHYoPuuQPj22JnVFtDMHmZDFkQ3vK15SrDUSTakh5fT4N8UVRf
Status: Ok
Log Messages:
Program 53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX invoke [1]
Program log: Instruction: Initialize
Program log: Third element = 30
Program 53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX consumed 1010 of 200000 compute units
Program 53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX success
Mappings
Unlike Solidity, Solana lacks a built-in mapping data structure. However, we can replicate the key-value mappings functionality in Solana by utilizing the HashMap type from the Rust standard library. Unlike EVM chains, the map we are demonstrating here is in memory, not storage. EVM chains do not have in-memory hash maps. We will demonstrate mappings in storage for Solana later.
Let’s see how to use HashMap
to create a mapping in Solana. Copy and paste the provided code snippet into the lib.rs file, and remember to replace the program ID with your own:
use anchor_lang::prelude::*;
declare_id!("53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX");
#[program]
pub mod tryrust {
use super::*;
// Import HashMap library
use std::collections::HashMap;
pub fn initialize(ctx: Context<Initialize>, key: String, value: String) -> Result<()> {
// Initialize the mapping
let mut my_map = HashMap::new();
// Add a key-value pair to the mapping
my_map.insert(key.to_string(), value.to_string());
// Log the value corresponding to a key from the mapping
msg!("My name is {}", my_map[&key]);
Ok(())
}
}
The my_map
variable is also made mutable so that we can edit it (i.e, add/remove key → value pairs). Also noticed how we imported HashMap
library?
Since the initialize
function receives two parameters, the test also needs to be updated:
it("Is initialized!", async () => {
// Add your test here.
const tx = await program.methods.initialize("name", "Bob").rpc();
console.log("Your transaction signature", tx);
});
When we run the test, we see the following log:
Transaction executed in slot 216142:
Signature: 5m4Cx26jaYT3c6YeJbLMDHppvki4Kmu3zTDMgk8Tao9v8b9sH7WgejETzymnHuUfr4hY25opptqniBuwDpncbnB9
Status: Ok
Log Messages:
Program 53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX invoke [1]
Program log: Instruction: Initialize
Program log: My name is Bob
Program 53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX consumed 2634 of 200000 compute units
Program 53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX success
Structs
In Solidity and Solana, structs are used to define custom data structures that can hold multiple fields. Let’s see a struct example in both Solidity and Solana.
In Solidity:
contract SolidityStructs {
// Defining a struct in Solidity
struct Person {
string my_name;
uint256 my_age;
}
// Creating an instance of the struct
Person person1;
function initPerson1(string memory name, uint256 age) public {
// Accessing and modifying struct fields
person1.my_name = name;
person1.my_age = age;
}
}
The 1-1 correspondence in Solana:
pub fn initialize(_ctx: Context<Initialize>, name: String, age: u64) -> Result<()> {
// Defining a struct in Solana
struct Person {
my_name: String,
my_age: u64,
}
// Creating an instance of the struct
let mut person1: Person = Person {
my_name: name,
my_age: age,
};
msg!("{} is {} years old", person1.my_name, person1.my_age);
// Accessing and modifying struct fields
person1.my_name = "Bob".to_string();
person1.my_age = 18;
msg!("{} is {} years old", person1.my_name, person1.my_age);
Ok(())
}
Exercise: update the test file to pass two arguments Alice and 20 to the initialize function and run the test, you should get the following logs:
Transaction executed in slot 324406:
Signature: 2XBQKJLpkJbVuuonqzirN9CK5dNKnuu5NqNCGTGgQovWBfrdjRcVeckDmqtzyEPe4PP8xSN8vf2STNxWygE4BPZN
Status: Ok
Log Messages:
Program 53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX invoke [1]
Program log: Instruction: Initialize
Program log: Alice is 20 years old
Program log: Bob is 18 years old
Program 53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX consumed 2601 of 200000 compute units
Program 53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX success
In the provided code snippet, the Solidity implementation stores the instance of a struct in storage, whereas in the Solana implementation, everything happened in the initialize function and nothing was stored on-chain. Storage will be discussed in a later tutorial.
Constants in Rust
Declaring a constant variable in Rust is straightforward. Instead of using the let keyword, use the const keyword. These can be declared outside of the #[program]
block.
use anchor_lang::prelude::*;
declare_id!("EiR8gcMCX11tYMRfoZ2vyheZsZ2NvdUTvYrRAUvTtYnL");
// *** CONSTANT DECLARED HERE ***
const MEANING_OF_LIFE_AND_EXISTENCE: u64 = 42;
#[program]
pub mod tryrust {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
msg!(&format!("Answer to the ultimate question: {}", MEANING_OF_LIFE_AND_EXISTENCE)); // new line here
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize {}
The usize type and casting
Most of the time we can assume unsigned integers are of type u64
in Solana, but there is an exception when measuring the length of a list: it will be of type usize. You will need to cast the variable as the following Rust code demonstrate:
use anchor_lang::prelude::*;
declare_id!("EiR8gcMCX11tYMRfoZ2vyheZsZ2NvdUTvYrRAUvTtYnL");
#[program]
pub mod usize_example {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
let mut dynamic_array: Vec<u32> = Vec::from([1,2,3,4,5,6]);
let len = dynamic_array.len(); // this has type usize
let another_var: u64 = 5; // this has type u64
let len_plus_another_var = len as u64 + another_var;
msg!("The result is {}", len_plus_another_var);
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize {}
Try Catch
Rust does not have try catch. Failures are expected to return errors (like we did in our tutorial on Solana reverts and errors) or panic for non-recoverable errors.
Exercise: Write a Solana / Rust program that takes a vector of u64, loops through it, and pushes all the even numbers into another vector, then prints the new vector.
Learn more with RareSkills
This tutorial is part of our free Solana course.
Originally Published February, 13, 2024