Solidity to Rust Series Pt. 2

EVM to Soroban Understanding Data Types

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

What are 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

What are Solidity Data Types?

In Solidity, we encounter a variety of data types, including:

  • Primitive Types: These are the basic data types in Solidity.
    • 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.
  • Composite Types: These types allow for more complex data structures.
    • 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

What Are Soroban Rust Data Types?

Soroban utilizes Rust data types, with a mix of familiar and unique types, including:

  • Primitive Types:
    • 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.
  • Composite Types:
    • 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.
  • Custom Types:
    • 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

What Are The Differences Between Frequently Used Solidity vs. Soroban Rust Data Types?

Both environments support mutable and constant data types, but there are nuances:

  • String vs. Symbol: Solidity uses string for variable-sized text, whereas Soroban uses Symbol for efficient, short strings or identifiers.

Solidity:

// 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!";
}

Soroban Rust:

#![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!")
    }
}
  • Addresses: Both platforms use addresses to denote accounts or contracts, but Soroban enhances functionality with direct authorization features.

Solidity:

// 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:

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()
    }
}
  • Bytes and Arrays: Similar concepts but with Rust-specific types like Bytes, BytesN, and Vec. In Soroban Rust, the equivalent of Solidity's arrays is typically represented using Rust's Vec type. While arrays in Solidity have a fixed size and are suitable for scenarios where the size of the collection is known in advance, Soroban Rust favors the more flexible Vec type for most use cases.

Solidity:

// 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];
    }
}

Soroban Rust:

#![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
    }
}
  • Structs and Enums: Both are used in similar ways to bundle data or define finite sets of values, with Rust providing additional variants and storage optimizations. These concepts are fundamental to smart contract development and are utilized extensively in both Solidity and Soroban Rust, as demonstrated in the following sections.

How are Data Types Used?

How To Declare State Variables in Solidity vs Soroban Rust

Solidity:

// 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:

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

How to Build a Struct in Solidity vs Soroban Rust

Solidity:

// 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; 
    }
}

Soroban Rust:

#![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

How to Use Enums in Solidity vs Soroban Rust

Solidity:

// 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;
    }
}

Soroban Rust:

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

How to Use Mapping in Solidity vs Soroban Rust

Solidity:

// 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];
    }
}

Soroban Rust:

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:

  • Map: This key represents a simple mapping from an address to a value. It contains a single element, which is an Address. This variant allows for mapping a specific address to a corresponding value.
  • NestedMap: This key represents a nested mapping from an address and an index to a boolean value. It contains two elements: an 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

Why Choose Rust Over 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

How to Migrate Smart Contracts from Ethereum’s Solidity to Soroban Rust

Check out Part One of The Essential Guide to Rust Development!