Stylus Rust SDK advanced features
This document provides information about advanced features included in the Stylus Rust SDK, that are not described in the previous pages. For information about deploying Rust smart contracts, see the cargo stylus CLI Tool. For a conceptual introduction to Stylus, see Stylus: A Gentle Introduction. To deploy your first Stylus smart contract using Rust, refer to the Quickstart.
Many of the affordances use macros. Though this section details what each does, it may be helpful to use cargo expand to see what they expand into if you’re doing advanced work in Rust.
Storage
This section provides extra information about how the Stylus Rust SDK handles storage. You can find more information and basic examples in Variables.
Rust smart contracts may use state that persists across transactions. There’s two primary ways to define storage, depending on if you want to use Rust or Solidity definitions. Both are equivalent, and are up to the developer depending on their needs.
#[storage]
The #[storage] macro allows a Rust struct to be used in persistent storage.
#[storage]
pub struct Contract {
owner: StorageAddress,
active: StorageBool,
sub_struct: SubStruct,
}
#[storage]
pub struct SubStruct {
// types implementing the `StorageType` trait.
}
Any type implementing the StorageType trait may be used as a field, including other structs, which will implement the trait automatically when #[storage] is applied. You can even implement StorageType yourself to define custom storage types. However, we’ve gone ahead and implemented the common ones.
| Type | Info |
|---|---|
StorageBool | Stores a bool |
StorageAddress | Stores an Alloy Address |
StorageUint | Stores an Alloy Uint |
StorageSigned | Stores an Alloy Signed |
StorageFixedBytes | Stores an Alloy FixedBytes |
StorageBytes | Stores a Solidity bytes |
StorageString | Stores a Solidity string |
StorageVec | Stores a vector of StorageType |
StorageMap | Stores a mapping of StorageKey to StorageType |
StorageArray | Stores a fixed-sized array of StorageType |
Every Alloy primitive has a corresponding StorageType implementation with the word Storage before it. This includes aliases, like StorageU256 and StorageB64.
sol_storage!
The types in #[storage] are laid out in the EVM state trie exactly as they are in Solidity. This means that the fields of a struct definition will map to the same storage slots as they would in EVM programming languages.
Because of this, it is often nice to define your types using Solidity syntax, which makes that guarantee easier to see. For example, the earlier Rust struct can re-written to:
sol_storage! {
pub struct Contract {
address owner; // becomes a StorageAddress
bool active; // becomes a StorageBool
SubStruct sub_struct,
}
pub struct SubStruct {
// other solidity fields, such as
mapping(address => uint) balances; // becomes a StorageMap
Delegate delegates[]; // becomes a StorageVec
}
}
The above will expand to the equivalent definitions in Rust, each structure implementing the StorageType trait. Many contracts, like our example ERC 20, do exactly this.
Because the layout is identical to Solidity’s, existing Solidity smart contracts can upgrade to Rust without fear of storage slots not lining up. You simply copy-paste your type definitions.
Note that one exception to this storage layout guarantee is contracts which utilize inheritance. The current solution in Stylus using #[borrow] and #[inherits(...)] packs nested (inherited) structs into their own slots. This is consistent with regular struct nesting in solidity, but not inherited structs. We plan to revisit this behavior in an upcoming release.
Existing Solidity smart contracts can upgrade to Rust if they use proxy patterns.
Consequently, the order of fields will affect the JSON ABIs produced that explorers and tooling might use. Most developers won’t need to worry about this though and can freely order their types when working on a Rust contract from scratch.