Solidity to Rust Series Pt. 2
Author
Julian Martinez
Publishing date
In this guide, we’ll dive into the similarities and differences between the data types in EVM’s Solidity and Soroban’s Rust. If you're looking to get more hands-on with building smart contracts, don’t forget to check out our previous piece on writing a “Hello World” contract in both languages here.
Data Types
Data types are critical in programming, serving to define the kind of data we deal with. They ensure data integrity, consistency, and efficient memory allocation, thus optimizing resource usage. Data types are categorized into primitives, like booleans and integers, and non-primitives, such as structs, mappings, and enums. Their correct use is essential for robust and efficient programming, particularly in smart contract development.
Solidity
In Solidity, we encounter a variety of data types, including:
bool
represents Boolean values (true
or false
).int
and uint
denote signed and unsigned integers, respectively, with varying sizes (e.g., int256
, uint256
).address
is used for Ethereum addresses.string
is for dynamic-length string data.bytes
for dynamic arrays of bytes, and bytes32
represents fixed-size byte arrays, useful for efficient storage.Arrays
can be either dynamic-size or fixed-size and support both primitive and composite types.struct
enables defining new types by grouping together variables (of both primitive and composite types).enum
allows the creation of custom types with a limited set of constant values.mapping
is a key-value store for associating unique keys with values.Soroban Rust
Soroban utilizes Rust data types, with a mix of familiar and unique types, including:
Bool (bool)
: Represents true or false values.32-bit Integers
: Signed (i32
) and unsigned (u32
).64-bit Integers
: Signed (i64
) and unsigned (u64
).128-bit Integers
: Signed (i128
) and unsigned (u128
).Symbol
: Small efficient strings up to 32 characters in length. Symbol::new
for strings up to 32 characters and symbol_short!
for strings up to 9 characters. Limited to characters a-zA-Z0-9_
and encoded into 64-bit integers.Bytes, Strings (Bytes, BytesN)
: Byte arrays and strings that can be passed to contracts and stores.Vec (Vec)
: A sequential and indexable growable collection type.Map (Map)
: An ordered key-value dictionary.Address (Address)
: A universal opaque identifier used in contracts.String (String)
: A contiguous growable array type containing u8s, requiring an environment to be passed in.Structs (with Named Fields)
: Custom types consisting of named fields stored on the ledger as a map of key-value pairs.Structs (with Unnamed Fields)
: Custom types consisting of unnamed fields stored on the ledger as a vector of values.Enum (Unit and Tuple Variants)
: Custom types consisting of unit and tuple variants stored on the ledger as a two-element vector, with the first element being the variant name and the second being the value.Enum (Integer Variants)
: Custom types consisting of integer variants stored on the ledger as the u32
value.Differences
Both environments support mutable and constant data types, but there are nuances:
// SPDX-License-Identifier: MIT
// compiler version must be greater than or equal to 0.8.24 and less than 0.9.0
pragma solidity ^0.8.24;
contract HelloWorld {
string public greet = "Hello, World!";
}
#![no_std]
use soroban_sdk::{contract, contractimpl, Env, Symbol};
#[contract]
pub struct HelloWorldContract;
#[contractimpl]
impl HelloWorldContract {
pub fn greet(env: &Env) -> Symbol {
Symbol::new(env, "Hello, World!")
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract AddressDemo {
function getMsgSender() public view returns (address) {
return msg.sender;
}
function getContractId() public view returns (address) {
return address(this);
}
}
Soroban Rust, being a deterministic and contained environment, does not have the concept of msg.sender as seen in Solidity. In Soroban, all operations are performed within the context of the Env struct, which encapsulates the environment of the smart contract execution. There are no global variables; instead, everything is accessed and manipulated through the Env struct.
In the example below, the addresses are accessed and manipulated directly within the contract's logic. The require_auth method is used to ensure that the call is made from the specified address, providing a layer of authentication and security within the contract.
#![no_std]
use soroban_sdk::{contract, contractimpl, Address, Env};
#[contract]
pub struct AddressDemo;
#[contractimpl]
impl AddressDemo {
pub fn get_user(addr: Address) -> Address {
// Uses the require_auth method to ensure that the call is made from the given address.
addr.require_auth();
addr
}
pub fn get_contract(env: &Env) -> Address {
env.current_contract_address()
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract ArrayAndBytes {
// Several ways to initialize an array
uint256[] public arr;
uint256[] public arr2 = [1, 2, 3];
// Fixed sized array, all elements initialize to 0 or values can be preset
// Array cannot exceed fixed size
uint256[10] public myFixedSizeArr;
uint256[5] public myFixedSizeArr2 = [1, 2, 3, 4, 5];
// Bytes (dynamic-size and fixed-size)
bytes public dynamicBytes = "hello, solidity";
bytes32 public fixedBytes = "hello, solidity";
function get(uint256 i) public view returns (uint256) {
return arr[i];
}
// Solidity can return the entire array.
// But this function should be avoided for
// arrays that can grow indefinitely in length.
function getArr() public view returns (uint256[] memory) {
return arr;
}
function push(uint256 i) public {
// Append to array
// This will increase the array length by 1.
arr.push(i);
}
function pop() public {
// Remove last element from array
// This will decrease the array length by 1
arr.pop();
}
function getLength() public view returns (uint256) {
return arr.length;
}
function remove(uint256 index) public {
// Delete does not change the array length.
// It resets the value at index to it's default value,
// in this case 0
delete arr[index];
}
}
#![no_std]
use soroban_sdk::{contract, contractimpl, contracttype, vec, Bytes, BytesN, Env, Vec};
// Define an enum to represent different data keys (state variable storage)
#[derive(Clone)]
#[contracttype]
pub enum DataKey {
Vec,
}
// Define the contract
#[contract]
pub struct ArrayAndBytesDemo;
#[contractimpl]
impl ArrayAndBytesDemo {
// Function to demonstrate dynamic bytes creation
pub fn dynamic_bytes_example(env: &Env) -> Bytes {
// Bytes (Bytes and BytesN)
let msg = "Hello, World!";
Bytes::from_slice(env, msg.as_bytes())
}
// Function to demonstrate fixed bytes creation; Results in a 32-byte array
pub fn fixed_bytes_example(env: &Env) -> BytesN<32> {
// Bytes (Bytes and BytesN)
let msg = "Hello, World!";
let msg_to_array = msg.as_bytes();
let mut msg_array = [0u8; 32];
msg_array[..msg_to_array.len()].copy_from_slice(&msg_to_array);
BytesN::from_array(env, &msg_array)
}
// Function to demonstrate creation of a Vec
pub fn vec_example(env: &Env) -> Vec<u32> {
// Vec
let vec: Vec<u32> = vec![&env, 0, 1, 2, 3];
vec
}
// Function to get a Vec from storage
pub fn get_vec(env: &Env) -> Vec<u32> {
// Vec
env.storage()
.persistent()
.get(&DataKey::Vec)
.unwrap_or(vec![&env, 0])
}
// Function to get an element from a Vec by index
pub fn get_vec_index(env: &Env, index: u32) -> u32 {
// Vec
let vec: Vec<u32> = env
.storage()
.persistent()
.get(&DataKey::Vec)
.unwrap_or(vec![&env, 0]);
vec.get(index).unwrap_or(0)
}
// Function to get the length of a Vec
pub fn get_vec_length(env: &Env) -> u32 {
// Vec
let vec: Vec<u32> = env
.storage()
.persistent()
.get(&DataKey::Vec)
.unwrap_or(vec![&env, 0]);
vec.len()
}
// Function to push an element into a Vec
pub fn push(env: &Env, value: u32) -> Vec<u32> {
// Vec
let mut vec = env
.storage()
.persistent()
.get(&DataKey::Vec)
.unwrap_or(vec![&env, 0]);
vec.push_back(value);
env.storage().persistent().set(&DataKey::Vec, &vec);
vec
}
// Function to remove the last element from a Vec
pub fn pop(env: &Env) -> Vec<u32> {
// Vec
let mut vec = env
.storage()
.persistent()
.get(&DataKey::Vec)
.unwrap_or(vec![&env, 0]);
vec.pop_back();
env.storage().persistent().set(&DataKey::Vec, &vec);
vec
}
// Function to remove an element from a Vec by index
pub fn remove(env: &Env, index: u32) -> Vec<u32> {
// Vec
let mut vec = env
.storage()
.persistent()
.get(&DataKey::Vec)
.unwrap_or(vec![&env, 0]);
vec.remove(index);
env.storage().persistent().set(&DataKey::Vec, &vec);
vec
}
}
How are Data Types Used?
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract MyContract {
bool public isActive = true;
uint256 public balance = 100;
string public name = "Alice";
// Function to deactivate the contract
function deactivate() public {
isActive = false;
}
// Function to activate the contract
function activate() public {
isActive = true;
}
// Function to update balance
function updateBalance(uint256 newBalance) public {
require(isActive, "Contract is not active");
balance = newBalance;
}
// Function to update name
function updateName(string memory newName) public {
require(isActive, "Contract is not active");
name = newName;
}
// Returns isActive
function getIsActive() public view returns (bool) {
return isActive;
}
// Returns balance
function getBalance() public view returns (uint256) {
return balance;
}
// Returns name
function getName() public view returns (string memory) {
return name;
}
}
Soroban Rust smart contracts commonly use enums to represent different aspects or states of the contract, especially for storing state variables.
#![no_std]
use soroban_sdk::{contract, contractimpl, contracttype, symbol_short, Env, Symbol};
// Define a contract type enum to represent different aspects of the contract
#[derive(Clone)]
#[contracttype]
pub enum DataKey {
IsActive,
Balance,
Name,
}
// Define the contract
#[contract]
pub struct MyContract;
#[contractimpl]
impl MyContract {
// Function to deactivate the contract
pub fn deactivate(env: &Env) {
env.storage().persistent().set(&DataKey::IsActive, &false); // Set the IsActive state to false
}
// Function to activate the contract
pub fn activate(env: &Env) {
env.storage().persistent().set(&DataKey::IsActive, &true); // Set the IsActive state to true
}
// Function to update the balance
pub fn update_balance(env: Env, new_balance: u128) {
let is_active: bool = env
.storage()
.persistent()
.get(&DataKey::IsActive)
.unwrap_or(false); // Get the IsActive state from storage or default to false
assert!(is_active, "Contract is not active"); // Ensure the contract is active
env.storage()
.persistent()
.set(&DataKey::Balance, &new_balance); // Update the balance in storage
}
// Function to update the name
pub fn update_name(env: Env, new_name: Symbol) {
let is_active: bool = env
.storage()
.persistent()
.get(&DataKey::IsActive)
.unwrap_or(false); // Get the IsActive state from storage or default to false
assert!(is_active, "Contract is not active"); // Ensure the contract is active
env.storage().persistent().set(&DataKey::Name, &new_name); // Update the name in storage
}
// Function to get the balance
pub fn get_balance(env: &Env) -> u128 {
env.storage()
.persistent()
.get(&DataKey::Balance)
.unwrap_or(0) // Get the balance from storage or default to 0
}
// Function to get the name
pub fn get_name(env: &Env) -> Symbol {
env.storage()
.persistent()
.get(&DataKey::Name)
.unwrap_or(symbol_short!("Unknown")) // Get the name from storage or default to "Unknown"
}
// Function to check if the contract is active
pub fn is_active(env: &Env) -> bool {
env.storage()
.persistent()
.get(&DataKey::IsActive)
.unwrap_or(false) // Get the IsActive state from storage or default to false
}
}
Struct
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
// This contract defines an account with a name and balance, and provides functions to update them.
contract AccountContract {
// Define a struct to represent an account with a name and balance
struct Account {
string name; // Name of the account holder
uint balance; // Balance of the account
}
// Declare a public variable of type Account to store the account details
Account public account;
// Constructor to initialize the account with a name and balance
constructor(string memory _name, uint _balance) {
account.name = _name; // Set the initial name
account.balance = _balance; // Set the initial balance
}
// Returns the account data
function get() public view returns (Account) {
return account;
}
// Function to update the balance of the account
function updateBalance(uint newBalance) public {
account.balance = newBalance;
}
// Function to update the name of the account holder
function updateName(string memory newName) public {
account.name = newName;
}
}
#![no_std]
use soroban_sdk::{contract, contractimpl, contracttype, symbol_short, Env, Symbol};
// Define a contract type for representing account data
#[contracttype]
pub struct Account {
pub name: Symbol, // Name of the account holder
pub balance: u128, // Balance of the account
}
// Define a contract type enum for representing data keys
#[derive(Clone)]
#[contracttype]
pub enum DataKey {
Account, // Represents the account data
}
// Define the main contract
#[contract]
pub struct MyContract;
#[contractimpl]
impl MyContract {
// Function to create a new account
pub fn new_account(env: &Env, new_name: Symbol, new_balance: u128) -> Account {
// Store the new account data
env.storage().persistent().set(
&DataKey::Account,
&Account {
name: new_name.clone(),
balance: new_balance,
},
);
// Return the created account
Account {
name: new_name.clone(),
balance: new_balance,
}
}
// Function to retrieve the account data
pub fn get_account(env: &Env) -> Account {
// Get the account data from storage or return default values if not found
env.storage()
.persistent()
.get(&DataKey::Account)
.unwrap_or(Account {
name: symbol_short!("none"),
balance: 0,
})
}
// Function to update the account balance
pub fn update_balance(env: &Env, new_balance: u128) {
// Get the current account data from storage
let account = env
.storage()
.persistent()
.get(&DataKey::Account)
.unwrap_or(Account {
name: symbol_short!("none"),
balance: 0,
});
// Update the account balance in storage
env.storage().persistent().set(
&DataKey::Account,
&Account {
name: account.name,
balance: new_balance,
},
);
}
// Function to update the account name
pub fn update_name(env: &Env, new_name: Symbol) {
// Get the current account data from storage
let account = env
.storage()
.persistent()
.get(&DataKey::Account)
.unwrap_or(Account {
name: symbol_short!("none"),
balance: 0,
});
// Update the account name in storage
env.storage().persistent().set(
&DataKey::Account,
&Account {
name: new_name,
balance: account.balance,
},
);
}
}
Enums
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract Enum {
// Enum representing shipping status
enum Status {
Pending,
Shipped,
Accepted,
Rejected,
Canceled
}
// Default value is the first element listed in
// definition of the type, in this case "Pending"
Status public status;
// Returns uint
// Pending - 0
// Shipped - 1
// Accepted - 2
// Rejected - 3
// Canceled - 4
function get() public view returns (Status) {
return status;
}
// Update status by passing uint into input
function set(Status _status) public {
status = _status;
}
// You can update to a specific enum like this
function cancel() public {
status = Status.Canceled;
}
// delete resets the enum to its first value, 0
function reset() public {
delete status;
}
}
This Soroban Rust smart contract, named StateContract
, is designed to manage the state of an order using the Status
enum. The Status enum defines various states of the order, including Pending
, Shipped
, Accepted
, Rejected
, and Canceled
. Each enum variant is associated with an integer value, which is stored as a u32
value on the ledger.
#![no_std]
use soroban_sdk::{contract, contractimpl, contracttype, Env};
// Enum to represent different data keys
#[derive(Clone)]
#[contracttype]
pub enum DataKey {
Status,
}
// Define the enum to represent different states
#[derive(Clone, Copy, PartialEq, Eq)]
#[contracttype]
pub enum Status {
Pending = 0,
Shipped = 1,
Accepted = 2,
Rejected = 3,
Canceled = 4,
}
#[contract]
pub struct StateContract;
#[contractimpl]
impl StateContract {
// Initialize the contract with the initial state
pub fn initialize(env: &Env) {
env.storage()
.persistent()
.set(&DataKey::Status, &Status::Pending);
}
// Function to get the current state of the contract
pub fn get_state(env: &Env) -> Status {
env.storage()
.persistent()
.get(&DataKey::Status)
.unwrap_or(Status::Pending)
}
// Function to set the state of the contract
pub fn set_state(env: &Env, new_state: Status) {
env.storage().persistent().set(&DataKey::Status, &new_state);
}
// }
// Function to set the state of the contract to shipped
pub fn ship(env: &Env) {
env.storage()
.persistent()
.set(&DataKey::Status, &Status::Shipped);
}
}
Mapping
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract Mapping {
// Mapping from address to uint
mapping(address => uint256) public myMap;
mapping(address => mapping(uint256 => bool)) public nested;
function get(address _addr) public view returns (uint256) {
// Mapping always returns a value.
// If the value was never set, it will return the default value.
return myMap[_addr];
}
function set(address _addr, uint256 _i) public {
// Update the value at this address
myMap[_addr] = _i;
}
function remove(address _addr) public {
// Reset the value to the default value.
delete myMap[_addr];
}
function getNestedMap(address _addr1, uint256 _i)
public
view
returns (bool)
{
// You can get values from a nested mapping
// even when it is not initialized
return nested[_addr1][_i];
}
function setNestedMap(
address _addr1,
uint256 _i,
bool _boo
) public {
nested[_addr1][_i] = _boo;
}
function removeNestedMap(address _addr1, uint256 _i) public {
delete nested[_addr1][_i];
}
}
In Soroban Rust, while there is a Map type available for mapping data, it's a common practice to use enums to represent different types of mappings.
In the provided Soroban Rust smart contract, the DataKey
enum is utilized to represent different storage keys for mapping data. It defines two distinct keys that take in different elements:
Address
. This variant allows for mapping a specific address to a corresponding value.Address
and a u64
index. This variant enables the creation of a more complex mapping structure where each address can have multiple boolean values associated with different indices.#![no_std]
use soroban_sdk::{contract, contractimpl, contracttype, Address, Env};
#[derive(Clone, PartialEq, Eq)]
#[contracttype]
pub enum DataKey {
Map(Address),
NestedMap(Address, u64),
}
#[contract]
pub struct MappingContract;
#[contractimpl]
impl MappingContract {
pub fn get(env: &Env, addr: Address) -> u64 {
env.storage()
.persistent()
.get(&DataKey::Map(addr))
.unwrap_or(0)
}
pub fn set(env: &Env, addr: Address, value: u64) {
env.storage().persistent().set(&DataKey::Map(addr), &value);
}
pub fn remove(env: &Env, addr: Address) {
env.storage().persistent().remove(&DataKey::Map(addr));
}
pub fn get_nested_map(env: &Env, addr: Address, index: u64) -> bool {
env.storage()
.persistent()
.get(&DataKey::NestedMap(addr, index))
.unwrap_or(false)
}
pub fn set_nested_map(env: &Env, addr: Address, index: u64, value: bool) {
env.storage()
.persistent()
.set(&DataKey::NestedMap(addr, index), &value);
}
pub fn remove_nested_map(env: &Env, addr: Address, index: u64) {
env.storage()
.persistent()
.remove(&DataKey::NestedMap(addr, index));
}
}
Rust v Solidity
Transitioning from Solidity to Soroban Rust involves understanding the differences and similarities in data types. While some concepts are directly translatable, Soroban introduces efficiencies and features tailored to its environment. For detailed documentation, please have a look at the Soroban documentation and join our Discord for real-life examples and more insight. Embrace the journey to leveraging Rust’s full potential in smart contract development. Happy coding!
Solidity to Rust Series Pt. 1
Check out Part One of The Essential Guide to Rust Development!