Skip to content

Adding structure of a contract to Rust SDK section #2095

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Apr 30, 2025
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
209 changes: 209 additions & 0 deletions arbitrum-docs/stylus/reference/project-structure.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
---
title: 'Structure of a Rust Contract'
description: 'A quick overview of how contracts are structured with the Stylus Rust SDK'
author: chrisco
sme: chrisco
sidebar_position: 1
target_audience: Developers using the Stylus Rust SDK to write and deploy smart contracts.
---

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](https://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](https://docs.arbitrum.io/stylus/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:

```shell
- 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](https://stylus-by-example.org/basic_examples/first_app) for an example of this. These macros are used to maintain [Solidity ABI](https://docs.soliditylang.org/en/v0.8.19/abi-spec.html#basic-design) compatibility to ensure that Rust contracts work with existing Solidity libraries and tooling.

`src/main.rs` is typically auto-generated by [cargo-stylus](https://github.com/OffchainLabs/cargo-stylus) and does not usually need to be modified. Its purpose is to assist with the generation of [JSON describing](https://docs.soliditylang.org/en/v0.8.19/abi-spec.html#json) 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](https://crates.io/crates/stylus-sdk) itself or import third-party packages from [crates.io](https://crates.io). See [First Steps with Cargo](https://doc.rust-lang.org/cargo/getting-started/first-steps.html) if you are new to Rust.

`rust-toolchain.toml` is used by public blockchain explorers, like [Arbiscan](https://arbiscan.io/), 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](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/index.html#structs) 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**

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

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

**Solidity-style Schema**

```rust
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:

```rust
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.

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

```rust
// import module
mod utils;

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

See [Defining modules](https://doc.rust-lang.org/book/ch07-02-defining-modules-to-control-scope-and-privacy.html) 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](https://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`:

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

[dependencies]
rust_decimal = "1.36.0"
```

Import the package into your contract:

```rust
// lib.rs
use rust_decimal_macros::dec;
```

Use imported types in your contract:

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

See [Using public Rust crates](https://docs.arbitrum.io/stylus/recommended-libraries#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](https://arbiscan.io/).

```rust
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.

```rust
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
}
}
```
5 changes: 5 additions & 0 deletions website/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,11 @@ const sidebars = {
id: 'stylus/reference/overview',
label: 'Overview',
},
{
type: 'doc',
id: 'stylus/reference/project-structure',
label: 'Structure of a Contract',
},
...stylusByExampleDocsSidebarSDK,
{
type: 'doc',
Expand Down