Skip to main content
Version: 1.1.0

Odra for Solidity developers

Introduction

Hi, stranger Solidity developer! If you are looking to expand your horizons into Rust-based smart contract development, you've come to the right place. Odra is a high-level framework designed to simplify the development of smart contracts for the Casper Network. This tutorial will guide you through the basics of transitioning from Solidity to Odra, highlighting key differences and providing practical examples. Before we delve into the details, we have great news for you. From the very beginning, we have been thinking of you. Our main goal was to design the framework in a way that flattens the learning curve, especially for Solidity developers.

Prerequisites

To follow this guide, you should have:

  • Knowledge of Solidity.
  • Familiarity with Ethereum and smart contract concepts.
  • Basic understanding of Rust, as Odra is based on it.

Hello World

Let's start with a simple "Hello World" contract in Odra. The following code snippet demonstrates a basic smart contract that stores a greeting message.

use odra::{prelude::*, Var};

#[odra::module]
pub struct HelloWorld {
greet: Var<String>,
}

#[odra::module]
impl HelloWorld {
pub fn init(&mut self, message: String) {
self.greet.set(message);
}

pub fn get(&self) -> String {
self.greet.get_or_default()
}
}

As you may have noticed, the Odra code is slightly more verbose than the Solidity code. To define a contract in Odra, you need to create a struct and implement a module for it, both annotated with the odra::module attribute. The struct contains the contract's state variables, while the module defines the contract's functions. In this example, the HelloWorld struct has a single state variable greet, which stores the greeting message. The module contains two functions: init to set the greeting message and get to retrieve it. Two key differences are:

  1. Odra does not generate getters for public state variables automatically, so you need to define them explicitly.
  2. To initialize values, you must do it in the init function, which is the contract constructor. You can't assign defaults outside the constructor.

Variable Storage and State Management

Data Types

use core::str::FromStr;
use odra::{
casper_types::{bytesrepr::Bytes, U256},
module::Module,
prelude::*,
Address, UnwrapOrRevert, Var,
};

#[odra::module]
pub struct Primitives {
boo: Var<bool>,
u: Var<u8>, // u8 is the smallest unsigned integer type
u2: Var<U256>, // U256 is the biggest unsigned integer type
i: Var<i32>, // i32 is the smallest signed integer type
i2: Var<i64>, // i64 is the biggest signed integer type
address: Var<Address>,
bytes: Var<Bytes>,
default_boo: Var<bool>,
default_uint: Var<U256>,
default_int: Var<i64>,
default_addr: Var<Address>,
}

#[odra::module]
impl Primitives {
pub fn init(&mut self) {
self.boo.set(true);
self.u.set(1);
self.u2.set(U256::from(456));
self.i.set(-1);
self.i2.set(456);
self.address.set(
Address::from_str(
"hash-d4b8fa492d55ac7a515c0c6043d72ba43c49cd120e7ba7eec8c0a330dedab3fb",
)
.unwrap_or_revert(&self.env()),
);
self.bytes.set(Bytes::from(vec![0xb5]));

let _min_int = U256::zero();
let _max_int = U256::MAX;
}

// For the types that have default values, we can use the get_or_default method
pub fn get_default_boo(&self) -> bool {
self.default_boo.get_or_default()
}

pub fn get_default_uint(&self) -> U256 {
self.default_uint.get_or_default()
}

pub fn get_default_int(&self) -> i64 {
self.default_int.get_or_default()
}

// Does not compile - Address does not have the default value
pub fn get_default_addr(&self) -> Address {
self.default_addr.get_or_default()
}
}

The range of integer types in Odra is slightly different from Solidity. Odra provides a wide range of integer types: u8, u16, u32, u64, U128, and U256 for unsigned integers, and i32 and i64 for signed integers.

The Address type in Odra is used to represent account and contract addresses. In Odra, there is no default/zero value for the Address type; the workaround is to use Option<Address>.

The Bytes type is used to store byte arrays.

Values are stored in units called Named Keys and Dictionaries. Additionally, local variables are available within the entry points and can be used to perform necessary actions or computations within the scope of each entry point.

Constants and Immutability

use odra::{casper_types::{account::AccountHash, U256}, Address};

#[odra::module]
pub struct Constants;

#[odra::module]
impl Constants {
pub const MY_UINT: U256 = U256([123, 0, 0, 0]);
pub const MY_ADDRESS: Address = Address::Account(
AccountHash([0u8; 32])
);
}

In Odra, you can define constants using the const keyword. Constants are immutable and can be of any type, including custom types. In addition to constants, Solidity also supports the immutable keyword, which is used to set the value of a variable once, in the constructor. Further attempts to alter this value result in a compile error. Odra/Rust does not have an equivalent to Solidity's immutable keyword.

Variables

use odra::{casper_types::U256, prelude::*, Var};

#[odra::module]
pub struct Variables {
text: Var<String>,
my_uint: Var<U256>,
}

#[odra::module]
impl Variables {
pub fn init(&mut self) {
self.text.set("Hello".to_string());
self.my_uint.set(U256::from(123));
}

pub fn do_something(&self) {
// Local variables
let i = 456;
// Env variables
let timestamp = self.env().get_block_time();
let sender = self.env().caller();
}
}

In Solidity there are three types of variables: state variables, local variables, and global variables. State variables are stored on the blockchain and are accessible by all functions within the contract. Local variables are not stored on the blockchain and are only available within the function in which they are declared. Global variables provide information about the blockchain. Odra uses very similar concepts, but with some differences. In Odra, state variables are a part of a module definition, and local variables are available within the entry points and can be used to perform necessary actions or computations within the scope of each entry point. Global variables are accessed using an instance of ContractEnv retrieved using the env() function.

Arrays and Mappings

use odra::{casper_types::U256, Address, Mapping};

#[odra::module]
pub struct MappingContract {
my_map: Mapping<Address, Option<U256>>
}

#[odra::module]
impl MappingContract {
pub fn get(&self, addr: Address) -> U256 {
// self.my_map.get(&addr) would return Option<Option<U256>>
// so we use get_or_default instead and unwrap the inner Option
self.my_map.get_or_default(&addr).unwrap_or_default()
}

pub fn set(&mut self, addr: Address, i: U256) {
self.my_map.set(&addr, Some(i));
}

pub fn remove(&mut self, addr: Address) {
self.my_map.set(&addr, None);
}
}

#[odra::module]
pub struct NestedMapping {
my_map: Mapping<(Address, U256), Option<bool>>
}

#[odra::module]
impl NestedMapping {
pub fn get(&self, addr: Address, i: U256) -> bool {
self.my_map.get_or_default(&(addr, i)).unwrap_or_default()
}

pub fn set(&mut self, addr: Address, i: U256, boo: bool) {
self.my_map.set(&(addr, i), Some(boo));
}

pub fn remove(&mut self, addr: Address, i: U256) {
self.my_map.set(&(addr, i), None);
}
}
use odra::{prelude::*, Var};

#[odra::module]
pub struct Array {
// the size of the array must be known at compile time
arr: Var<[u8; 10]>,
vec: Var<Vec<u32>>,
}

#[odra::module]
impl Array {
pub fn init(&mut self) {
self.arr.set([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
self.vec.set(vec![1, 2, 3, 4, 5]);
}

pub fn get_arr(&self) -> [u8; 10] {
self.arr.get_or_default()
}

pub fn push_vec(&mut self, value: u32) {
let mut vec = self.vec.get_or_default();
vec.push(value);
self.vec.set(vec);
}

pub fn pop_vec(&mut self) {
let mut vec = self.vec.get_or_default();
vec.pop();
self.vec.set(vec);
}

pub fn update_arr(&mut self, index: u8, value: u8) {
let mut arr = self.arr.get_or_default();
arr[index as usize] = value;
self.arr.set(arr);
}
}

For storing a collection of data as a single unit, Odra uses the Vec type for dynamic arrays and fixed-size arrays, both wrapped with the Var container. As in Solidity, you must be aware that reading the entire array in one go can be expensive, so it's better to avoid it for large arrays. In many cases, you can use a Mapping or List instead of an array or vector to store data.

Custom types

use odra::{prelude::*, Var};

#[odra::odra_type]
#[derive(Default)]
pub enum Status {
#[default]
Pending,
Shipped,
Accepted,
Rejected,
Canceled,
}

#[odra::module]
pub struct Enum {
status: Var<Status>,
}

#[odra::module]
impl Enum {
pub fn get(&self) -> Status {
self.status.get_or_default()
}

pub fn set(&mut self, status: Status) {
self.status.set(status);
}

pub fn cancel(&mut self) {
self.status.set(Status::Canceled);
}

pub fn reset(&mut self) {
self.status.set(Default::default());
}
}

In Odra, custom types are defined using the #[odra::odra_type] attribute. The enum can have a default value specified using the #[default] attribute if derived from the Default trait. The enum can be used as a state variable in a contract, and its value can be set and retrieved using the set and get functions. The value cannot be deleted; however, it can be set using the Default::default() function.

use odra::{prelude::*, List};

#[odra::odra_type]
pub struct Todo {
text: String,
completed: bool,
}

#[odra::module]
pub struct Enum {
// You could also use Var<Vec<Todo>> instead of List<Todo>,
// but List is more efficient for large arrays,
// it loads items lazily.
todos: List<Todo>,
}

#[odra::module]
impl Enum {
pub fn create(&mut self, text: String) {
self.todos.push(Todo {
text,
completed: false,
});
}

pub fn update_text(&mut self, index: u32, text: String) {
if let Some(mut todo) = self.todos.get(index) {
todo.text = text;
self.todos.replace(index, todo);
}
}

pub fn toggle_complete(&mut self, index: u32) {
if let Some(mut todo) = self.todos.get(index) {
todo.completed = !todo.completed;
self.todos.replace(index, todo);
}
}

// Odra does not create getters by default
pub fn get(&self, index: u32) -> Option<Todo> {
self.todos.get(index)
}
}

Similarly to enums, custom structs are defined using the #[odra::odra_type] attribute. The struct can be used to define a list of items in a contract. The list can be created using the List type, which is more efficient for large arrays as it loads items lazily.

Data Location

In Solidity, data location is an important concept that determines where the data is stored and how it can be accessed. The data location can be memory, storage, or calldata. In Odra, data location is not explicitly defined, but whenever interacting with storage primitives (e.g., Var, Mapping, List), the data is stored in the contract's storage.

Functions

Odra contracts define their entry point and internal functions within the impl block. Here's an example of a transfer function:

impl Erc20 {
pub fn transfer(&mut self, recipient: &Address, amount: &U256) {
self.internal_transfer(&self.env().caller(), recipient, amount);
// Transfer logic goes here
}

fn internal_transfer(&mut self, sender: &Address, recipient: &Address, amount: &U256) {
// Internal transfer logic goes here
}
}

Functions can modify contract state and emit events using the ContractEnv function.

View and Pure

use odra::Var;

#[odra::module]
pub struct ViewAndPure {
x: Var<u32>
}

#[odra::module]
impl ViewAndPure {
pub fn add_to_x(&self, y: u32) -> u32 {
self.x.get_or_default() + y
}
}

pub fn add(i: u32, j: u32) -> u32 {
i + j
}

In Odra, you don't need to specify view or pure functions explicitly. All functions are considered view functions by default, meaning they can read contract state but not modify it. To modify the state, the first parameter (called the receiver parameter) should be &mut self. If you want to create a pure function that doesn't read or modify state, you can define it as a regular Rust function without any side effects.

Modifiers

use odra::{module::Module, Var};

#[odra::module]
pub struct FunctionModifier {
x: Var<u32>,
locked: Var<bool>,
}

#[odra::module]
impl FunctionModifier {
pub fn decrement(&mut self, i: u32) {
self.lock();
self.x.set(self.x.get_or_default() - i);

if i > 1 {
self.decrement(i - 1);
}
self.unlock();
}

#[inline]
fn lock(&mut self) {
if self.locked.get_or_default() {
self.env().revert(Error::NoReentrancy);
}

self.locked.set(true);
}

#[inline]
fn unlock(&mut self) {
self.locked.set(false);
}
}

#[odra::odra_error]
pub enum Error {
NoReentrancy = 1,
}

In Odra, there is no direct equivalent to Solidity's function modifiers. Instead, you can define functions that perform certain actions before or after the main function logic. In the example above, the lock and unlock functions are called before and after the decrement function, respectively, but they must be called explicitly.

As often as practicable, developers should inline functions by including the body of the function within their code using the #[inline] attribute. In the context of coding for Casper blockchain purposes, this reduces the overhead of executed Wasm and prevents unexpected errors due to exceeding resource tolerances.

Visibility

Functions and state variables have to declare whether they are accessible by other contracts.

Functions can be declared as:

`pub` inside `#[odra::module]` impl block - any contract/submodule and account can call.
`pub` inside a regular impl block - any submodule can call.
`default/no modifier/private` - only inside the contract that defines the function.

Payable

use odra::{casper_types::U512, prelude::*, Address, ExecutionError, Var};

#[odra::module]
pub struct Payable {
owner: Var<Address>,
}

#[odra::module]
impl Payable {
pub fn init(&mut self) {
self.owner.set(self.env().caller());
}

#[odra(payable)]
pub fn deposit(&self) {
}

pub fn not_payable(&self) {
}

pub fn withdraw(&self) {
let amount = self.env().self_balance();
self.env().transfer_tokens(&self.owner.get_or_revert_with(ExecutionError::UnwrapError), &amount);
}

pub fn transfer(&self, to: Address, amount: U512) {
self.env().transfer_tokens(&to, &amount);
}
}

In Odra, you can define a function with the #[odra(payable)] attribute to indicate that the function can receive CSPRs. In Solidity, the payable keyword is used to define functions that can receive Ether.

Selectors

In Solidity, when a function is called, the first 4 bytes of calldata specify which function to call. This is called a function selector.

contract_addr.call(
abi.encodeWithSignature("transfer(address,uint256)", address, 1234)
)

Odra does not support such a mechanism. You must have access to the contract interface to call a function.

Events and Logging

use odra::{prelude::*, Address};

#[odra::event]
pub struct Log {
sender: Address,
message: String,
}

#[odra::event]
pub struct AnotherLog {}

#[odra::module]
struct Event;

#[odra::module]
impl Event {
pub fn test(&self) {
let env = self.env();
env.emit_event(Log {
sender: env.caller(),
message: "Hello World!".to_string(),
});
env.emit_event(Log {
sender: env.caller(),
message: "Hello Casper!".to_string(),
});
env.emit_event(AnotherLog {});
}
}

In Odra, events are regular structs defined using the #[odra::event] attribute. The event struct can contain multiple fields, which can be of any type (primitive or custom Odra type). To emit an event, use the env's emit_event() function, passing the event struct as an argument.

note

Events in Solidity are used to emit logs that off-chain services can capture. However, Casper does not support events natively. Odra mimics this feature. Read more about it in the Basics section.

Error Handling

use odra::{prelude::*, casper_types::{U256, U512}};

#[odra::odra_error]
pub enum CustomError {
InsufficientBalance = 1,
InputLowerThanTen = 2,
}

#[odra::module]
pub struct Error;

#[odra::module]
impl Error {
pub fn test_require(&mut self, i: U256) {
if i <= 10.into() {
self.env().revert(CustomError::InputLowerThanTen);
}
}

pub fn execute_external_call(&self, withdraw_amount: U512) {
let balance = self.env().self_balance();
if balance < withdraw_amount {
self.env().revert(CustomError::InsufficientBalance);
}
}
}

In Solidity, there are four ways to handle errors: require, revert, assert, and custom errors. In Odra, there is only one way to revert the execution of a function - by using the env().revert() function. The function takes an error type as an argument and stops the execution of the function. You define an error type using the #[odra::odra_error] attribute. On Casper, an error is only a number, so you can't pass a message with the error.

Composition vs. Inheritance

In Solidity, developers often use inheritance to reuse code and establish relationships between contracts. However, Odra and Rust follow a different paradigm known as composition. Instead of inheriting behavior from parent contracts, Odra encourages the composition of contracts by embedding one contract within another.

Let's take a look at the difference between inheritance in Solidity and composition in Odra.

use odra::{prelude::*, SubModule};

#[odra::module]
pub struct A;

#[odra::module]
impl A {
pub fn foo(&self) -> String {
"A".to_string()
}
}

#[odra::module]
pub struct B {
a: SubModule<A>
}

#[odra::module]
impl B {
pub fn foo(&self) -> String {
"B".to_string()
}
}

#[odra::module]
pub struct C {
a: SubModule<A>
}

#[odra::module]
impl C {
pub fn foo(&self) -> String {
"C".to_string()
}
}

#[odra::module]
pub struct D {
b: SubModule<B>,
c: SubModule<C>
}

#[odra::module]
impl D {
pub fn foo(&self) -> String {
self.c.foo()
}
}

#[odra::module]
pub struct E {
b: SubModule<B>,
c: SubModule<C>
}

#[odra::module]
impl E {
pub fn foo(&self) -> String {
self.b.foo()
}
}

#[odra::module]
pub struct F {
a: SubModule<A>,
b: SubModule<B>,
}

#[odra::module]
impl F {
pub fn foo(&self) -> String {
self.a.foo()
}
}

Solidity supports both single and multiple inheritance. This means a contract can inherit from one or more contracts. Solidity uses a technique called "C3 linearization" to resolve the order in which base contracts are inherited in the case of multiple inheritance. This helps to ensure a consistent method resolution order. However, multiple inheritance can lead to complex code and potential issues, especially for inexperienced developers.

In contrast, Rust does not have a direct equivalent to the inheritance model, but it achieves similar goals through composition. Each contract is defined as a struct, and contracts can be composed by embedding one struct within another. This approach provides a more flexible and modular way to reuse code and establish relationships between contracts.

Libraries and Utility

use odra::{casper_types::U256, prelude::*, UnwrapOrRevert, Var};

mod math {
use odra::casper_types::U256;

pub fn sqrt(y: U256) -> U256 {
let mut z = y;
if y > 3.into() {
let mut x = y / 2 + 1;
while x < z {
z = x;
x = (y / x + x) / 2;
}
} else if y != U256::zero() {
z = U256::one();
}
z
}
}

#[odra::module]
struct TestMath;

#[odra::module]
impl TestMath {
pub fn test_square_root(&self, x: U256) -> U256 {
math::sqrt(x)
}
}

#[odra::odra_error]
enum Error {
EmptyArray = 100,
}

trait Removable {
fn remove(&mut self, index: usize);
}

impl Removable for Var<Vec<U256>> {
fn remove(&mut self, index: usize) {
let env = self.env();
let mut vec = self.get_or_default();
if vec.is_empty() {
env.revert(Error::EmptyArray);
}
vec[index] = vec.pop().unwrap_or_revert(&env);
self.set(vec);
}
}

#[odra::module]
struct TestArray {
arr: Var<Vec<U256>>,
}

#[odra::module]
impl TestArray {
pub fn test_array_remove(&mut self) {
let mut arr = self.arr.get_or_default();
for i in 0..3 {
arr.push(i.into());
}
self.arr.set(arr);

self.arr.remove(1);

let arr = self.arr.get_or_default();
assert_eq!(arr.len(), 2);
assert_eq!(arr[0], 0.into());
assert_eq!(arr[1], 2.into());
}
}

In Solidity, libraries are similar to contracts but can't declare any state variables and can't receive Ether. In the sample code above, the Math library contains a square root function, while the Array library provides a function to remove an element from an array. Both libraries are consumed in different ways: the TestMath contract calls the sqrt function directly, while the TestArray contract uses the using keyword, which extends the type uint256[] by adding the remove function.

In Odra, you use language-level features: modules and traits. The mod keyword defines a module, which is similar to a library in Solidity. Modules can contain functions, types, and other items that can be reused across multiple contracts. Traits are similar to interfaces in other programming languages, defining a set of functions that a type must implement. Implementing the Removable trait for the Var<Vec<U256>> type allows the remove function to be called on a variable that stores a vector of U256 values.

Fallback and Receive Functions

In Solidity, a contract receiving Ether must implement a receive() and/or fallback() function. The receive() function is called when Ether is sent to the contract with no data, while the fallback() function is called when the contract receives Ether with data or when a function that does not exist is called.

Odra does not have a direct equivalent to the receive() and fallback() functions. Instead, you can define a function with the #[odra(payable)] attribute to indicate that the function can receive CSPRs.

Miscellaneous

Hashing

use odra::{
casper_types::{bytesrepr::ToBytes, U256},
prelude::*,
Address, UnwrapOrRevert, Var,
};

#[odra::module]
pub struct HashFunction;

#[odra::module]
impl HashFunction {
pub fn hash(&self, text: String, num: U256, addr: Address) -> [u8; 32] {
let env = self.env();
let mut data = Vec::new();
data.extend(text.to_bytes().unwrap_or_revert(&env));
data.extend(num.to_bytes().unwrap_or_revert(&env));
data.extend(addr.to_bytes().unwrap_or_revert(&env));
env.hash(data)
}
}

#[odra::module]
pub struct GuessTheMagicWord {
answer: Var<[u8; 32]>,
}

#[odra::module]
impl GuessTheMagicWord {
/// Initializes the contract with the magic word hash.
pub fn init(&mut self) {
self.answer.set([
0x86, 0x67, 0x15, 0xbb, 0x0b, 0x96, 0xf1, 0x06, 0xe0, 0x68, 0x07, 0x89, 0x22, 0x84,
0x42, 0x81, 0x19, 0x6b, 0x1e, 0x61, 0x45, 0x50, 0xa5, 0x70, 0x4a, 0xb0, 0xa7, 0x55,
0xbe, 0xd7, 0x56, 0x08,
]);
}

/// Checks if the `word` is the magic word.
pub fn guess(&self, word: String) -> bool {
let env = self.env();
let hash = env.hash(word.to_bytes().unwrap_or_revert(&env));
hash == self.answer.get_or_default()
}
}

The key difference between the two is that in Solidity, the keccak256 function is used to hash data, while in Odra, the env.hash() function is used, which implements the blake2b algorithm. Both functions take a byte array as input and return a 32-byte hash.

Try-catch

use odra::{module::Module, Address, ContractRef, Var};

#[odra::module]
pub struct Example {
other_contract: Var<Address>,
}

#[odra::module]
impl Example {
pub fn init(&mut self, other_contract: Address) {
self.other_contract.set(other_contract);
}

pub fn execute_external_call(&self) {
if let Some(addr) = self.other_contract.get() {
let result = OtherContractContractRef::new(self.env(), addr).some_function();
match result {
Ok(success) => {
// Code to execute if the external call was successful
}
Err(reason) => {
// Code to execute if the external call failed
}
}
}
}
}

#[odra::module]
pub struct OtherContract;

#[odra::module]
impl OtherContract {
pub fn some_function(&self) -> Result<bool, ()> {
Ok(true)
}
}

In Solidity, try/catch is a feature that allows developers to handle exceptions and errors more gracefully. The try/catch statement allows developers to catch and handle exceptions that occur during external function calls and contract creation.

In Odra, there is no direct equivalent to the try/catch statement in Solidity. However, you can use the Result type to handle errors in a similar way. The Result type is an enum that represents either success (Ok) or failure (Err). You can use the match statement to handle the Result type and execute different code based on the result. However, if an unexpected error occurs on the way, the whole transaction reverts.

Conclusion

Congratulations! You've now learned the main differences in writing smart contracts with the Odra Framework. By understanding the structure, initialization, error handling, and the composition pattern in Odra, you can effectively transition from Solidity to Odra for Casper blockchain development.

Experiment with the provided code samples, explore more advanced features, and unleash the full potential of the Odra Framework.

Read more about the Odra Framework in the Basics and Advanced sections.

Learn by example with our Tutorial series, you will find there a contract you likely familiar with - the Erc20 standard implementation.

If you have any further questions or need clarification on specific topics, feel free to join our Discord!