Skip to main content

Structure of a Rust Contract

Contracts in Rust are similar to contracts in Solidity. Each contract can contain declarations of State Variables, Functions, Function Modifiers, Events, Errors, Struct Types, and Enum Types. In addition, Rust contracts can import third-party packages from crates.io as dependencies and use them for advanced functionality.

Project Layout

In the most basic example, this is how a Rust contract will be organized. The simplest way to get going with a new project is to follow the Quickstart guide, or if you've already installed all dependencies, just run cargo stylus new <YOUR_PROJECT_NAME> from your terminal to begin a new project. Once installed, your project will include the following required files:

- src
- lib.rs
- main.rs
- Cargo.toml
- rust-toolchain.toml

src/lib.rs is the root module of your contract's code. Here, you can import utilities or methods from internal or external modules, define the data layout of your contract's state variables, and define your contract's public API. This module must define a root data struct with the #[entrypoint] macro and provide an impl block annotated with #[public] to define public or external methods. See First App for an example of this. These macros are used to maintain Solidity ABI compatibility to ensure that Rust contracts work with existing Solidity libraries and tooling.

src/main.rs is typically auto-generated by cargo-stylus and does not usually need to be modified. Its purpose is to assist with the generation of JSON describing your contract's public interface, for use with automated tooling and frontend frameworks.

Cargo.toml is standard file that Rust projects use to define a package's name, repository location, etc as well as import dependencies and define feature and build flags. From here you can define required dependencies such as the Stylus SDK itself or import third-party packages from crates.io. See First Steps with Cargo if you are new to Rust.

rust-toolchain.toml is used by public blockchain explorers, like Arbiscan, to assist with source code verification. To ensure that source code can be compiled deterministically, we use this file to include relevant metadata like what version of Rust was used.

Your contract may also include other dot files (such as .gitignore, .env, etc), markdown files for docs, or additional subfolders.

State Variables

Like Solidity, Rust contracts are able to define state variables. These are variables which are stored on the chain's state trie, which is essentially the chain's database. They differ from standard Rust variables in that they must implement the Storage trait from the Stylus SDK. This trait is used to layout the data in the trie in a Solidity-compatible fashion. The Stylus SDK provides Storage types for all Solidity primitives out-of-the-box, such as StorageAddress, StorageU256, etc. See storage module for more information.

When working with state variables, you can either use Rust-style syntax or Solidity-style syntax to define your data schema. The #[storage] macro is used to define Rust-style state variables while sol_storage! macro is used for Solidity-style state variables. Both styles may have more than one struct but must annotate a single struct as the root struct with #[entrypoint] macro. Below are examples of each.

Rust-style Schema

use stylus_sdk::{prelude::*, storage::{StorageU256, StorageAddress}};

#[storage]
#[entrypoint]
pub struct MyContract {
owner: StorageAddress,
version: StorageU256,
}

Solidity-style Schema

use stylus_sdk::{prelude::*};

sol_storage! {
#[entrypoint]
pub struct MyContract {
owner: address,
version: uint256,
}
}

To read from state or write to it, getters and setters are used:

let new_count = self.count.get() + U256::from(1);
self.count.set(new_count);

Functions

Contract functions are defined by providing an impl block for your contract's #[entrypoint] struct and annotating that block with #[public] to make the functions part of the contract's public API. The first parameter of each function is &self, which references the struct annotated with #[entrypoint], it's used for reading state variables. By default, methods are view-only and cannot mutate state. To make a function mutable and able to alter state, &mut self must be used. Internal methods can be defined on a separate impl block for the struct that is not annotated with #[public]. Internal methods can access state.

// Defines the public, external methods for your contract
// This impl block must be for the #[entrypoint] struct defined prior
#[public]
impl Counter {
// By annotating first arg with &self, this indicates a view function
pub fn get(&self) -> U256 {
self.count.get()
}

// By annotating with &mut self, this is a mutable public function
pub fn set_count(&mut self, count: U256) {
self.count.set(count);
}
}

// Internal methods (NOT part of public API)
impl Counter {
fn add(a: U256, b: U256) -> U256 {
a + b
}
}

Modules

Modules are a way to organize code into logical units. While your contract must have a lib.rs which defines your entrypoint struct, you can also define utility functions, structs, enums, etc, in modules and import them in to use in your contract methods.

For example, with this file structure:

- src
- lib.rs
- main.rs
- utils
- mod.rs
- Cargo.toml
- rust-toolchain.toml

In lib.rs:

// import module
mod utils;

// ..other code
const score = utils::check_score();

See Defining modules in the Rust book for more info on modules and how to use them.

Importing Packages

Rust has a robust package manager for managing dependencies and importing 3rd party libraries to use in your smart contracts. These packages (called crates in Rust) are located at crates.io. To make use of a dependency in your code, you'll need to complete these steps:

Add the package name and version to your Cargo.toml:

# Cargo.toml
[package]
# ...package info here

[dependencies]
rust_decimal = "1.36.0"

Import the package into your contract:

// lib.rs
use rust_decimal_macros::dec;

Use imported types in your contract:

// create a fixed point Decimal value
let price = dec!(72.00);

See Using public Rust crates for more important details on using public Rust crates as well as a curated list of crates that tend to work well for smart contract development.

Events

Events are used to publicly log values to the EVM. They can be useful for users to understand what occurred during a transaction while inspecting a transaction on a public explorer, like Arbiscan.

sol! {
event HighestBidIncreased(address bidder, uint256 amount);
}

#[public]
impl AuctionContract {
pub fn bid() {
// ...
evm::log(HighestBidIncreased {
bidder: Address::from([0x11; 20]),
amount: U256::from(42),
});
}
}

Errors

Errors allow you to define descriptive names for failure situations. These can be useful for debugging or providing users with helpful information for why a transaction may have failed.

sol! {
error NotEnoughFunds(uint256 request, uint256 available);
}

#[derive(SolidityError)]
pub enum TokenErrors {
NotEnoughFunds(NotEnoughFunds),
}

#[public]
impl Token {
pub fn transfer(&mut self, to: Address, amount: U256) -> Result<(), TokenErrors> {
const balance = self.balances.get(msg::sender());
if (balance < amount) {
return Err(TokenErrors::NotEnoughFunds(NotEnoughFunds {
request: amount,
available: balance,
}));
}
// .. other code here
}
}