You’re likely already familiar with our Contracts Library for Stylus, released in October 2024. While Arbitrum Stylus delivers faster execution and lower gas fees, tools for unit testing smart contracts are limited. Testing is essential to ensure smart contracts function as intended and to minimize the risk of exploits. The immutable nature of blockchain technology requires rigorous security measures, making thorough testing a vital part of the development process.
To address this need, we developed Mostu
, a useful test suite for Rust smart contracts. Mostu
simplifies the process of writing tests, empowering developers to ensure the reliability of their code. In this guide, we’ll walk you through the steps to create a new project, integrate our Contracts library, and set up your test environment.
A step-by-step guide to using Motsu for smarter testing in Rust
1 - Install Rust
If you don't have it installed, follow these instructions.
2 - Set up the environment
Run the following commands in your terminal to set the Rust version to 1.80 and install Stylus cli.
rustup default 1.80
cargo install --force cargo-stylus
3 - Create and verify the project
Run this command to initialize a minimal project in the current folder avoiding extra files and folders, having a clear structure to start.
cargo stylus new MyToken --minimal
Add the build target to ensure compatibility with the desired deployment environment and proper functioning of the compiled contracts.
rustup target add wasm32-unknown-unknown
Add the following dependencies to your Cargo.toml
[dependencies]
stylus-sdk = "0.6.0"
openzeppelin-stylus = "0.1.1"
alloy-sol-types = "0.7.6"
Write your contract code in src/lib.rs
. This example demonstrates the implementation of a simple ERC-20 contract with metadata (name and symbol) and an owner-only permission to mint tokens. The initial section of the code defines the errors that the contract may throw, which will also be used for verification during testing. The final section implements the token contract, inheriting functionality from three contracts inopenzeppelin_stylus
library: Erc20
, Erc20Metadata
, and Ownable
.
use openzeppelin_stylus::{access::ownable::Ownable, token::erc20::{extensions::Erc20Metadata, Erc20}};
use alloy_sol_types::sol;
use stylus_sdk::{alloy_primitives::{Address, U256}, prelude::{entrypoint, public, sol_storage}, stylus_proc::SolidityError};// Error definition, important to test when the contract should fail
sol! {
#[derive(Debug)]
#[allow(missing_docs)]
error OwnableUnauthorizedAccount(address account);
#[derive(Debug)]
#[allow(missing_docs)]
error OwnableErrorAccount(address account);
#[derive(Debug)]
#[allow(missing_docs)]
error MintInvalidReceiver(address receiver);
#[derive(Debug)]
#[allow(missing_docs)]
error MintOperationError(address account, uint256 value);
}
#[derive(SolidityError, Debug)]
pub enum Error {
UnauthorizedAccount(OwnableUnauthorizedAccount),
OwnableError(OwnableErrorAccount),
InvalidReceiver(MintInvalidReceiver),
MintError(MintOperationError),
}// Token implementation
#[entrypoint]
#[storage]struct Erc20Example {
#[borrow]
pub erc20: Erc20,
#[borrow]
pub metadata: Erc20Metadata,
#[borrow]
pub ownable: Ownable,
}
#[public]
#[inherit(Erc20,Erc20Metadata,Ownable)]
impl Erc20Example {
// Add token minting feature and verifies ownership
pub fn mint(
&mut self,
account: Address,
value: U256,
) -> Result<(), Error> {
self.ownable.only_owner().map_err(|err| match err {
openzeppelin_stylus::access::ownable::Error::UnauthorizedAccount(_) =>
Error::UnauthorizedAccount(OwnableUnauthorizedAccount{account}),
_=> Error::OwnableError(OwnableErrorAccount{account}),
})?;self.erc20._mint(account, value).map_err(|err| match err {
openzeppelin_stylus::token::erc20::Error::InvalidReceiver(_) =>
Error::InvalidReceiver(MintInvalidReceiver{receiver: account}),
_=> Error::MintError(MintOperationError{account,value}),
})?;Ok(())
}
}
Verify the code running the following command in your terminal
cargo stylus check
As testing in Stylus is currently limited, Motsu
was created to provide a set of helpers specifically for testing OpenZeppelin Contracts for Stylus and it has been published as an open-source standalone crate for the community. With Motsu
, you can write unit tests for your contracts just like regular Rust tests, abstracting the necessary setup and providing access to VM affordances through the #[motsu::test]
macro.
To start, add the library to Cargo.toml
[dependencies]
...
motsu = "0.2.1"
Begin with a simple, short test to ensure the test environment is set up correctly.
Here’s an example to verify that balance_of returns a zero balance when no funds are present.
#[cfg(test)]
mod tests {
use openzeppelin_stylus::token::erc20::IErc20;
use stylus_sdk::{alloy_primitives::{address, uint}, msg};
use crate::Erc20Example;
#[motsu::test]
fn initial_balance_is_zero(contract: Erc20Example) {
let test_address = address!("1234567891234567891234567891234567891234");
let zero = uint!(0_U256);
let balance = contract.erc20.balance_of(test_address);
assert_eq!(balance, zero);
}
}
Once everything is working, add tests for every function you’ve implemented in the contract. Test with various input values, and ensure the functions fail as expected in edge cases. There’s no need to test inherited contracts like ERC20
, ERC20Metadata
, or Ownable
, as they are already thoroughly tested through Motsu
.
Below, you will find three example tests: one to verify that the owner can mint tokens, another to check that it is not possible to mint tokens to the zero address, and a final one to confirm that non-owner accounts can not mint tokens.
E.g. 1 Verify that the owner can mint tokens
#[motsu::test]
fn owner_mints_tokens(contract: Erc20Example) {
let test_address = address!("1234567891234567891234567891234567891234");
let ten = uint!(10_U256);
contract.ownable._owner.set(msg::sender());
let _ = contract.mint(test_address,ten);
let balance = contract.erc20.balance_of(test_address);
assert_eq!(balance, ten);
}
E.g. 2 Verify is not possible to mint tokens to the zero address
#[motsu::test]
fn owner_mints_tokens_to_address_zero(contract: Erc20Example) {
let test_address = Address::ZERO;
let ten = uint!(10_U256);
contract.ownable._owner.set(msg::sender());
let result = contract.mint(test_address,ten);
assert!(matches!(result,Err(Error::InvalidReceiver(_))))
}
E.g. 3 Confirm that non-owner accounts can not mint tokens.
#[motsu::test]
fn not_owner_tries_to_mint_tokens(contract: Erc20Example) {
let test_address = address!("1234567891234567891234567891234567891234");
let not_owner_address = address!("9123456789123456789123456789123456789123");
let ten = uint!(10_U256);
contract.ownable._owner.set(not_owner_address);
let result = contract.mint(test_address,ten);
assert!(matches!(result,Err(Error::UnauthorizedAccount(_))))
}
By integrating Motsu
into your development workflow, you can ensure your Rust smart contracts are robust, secure, and deployment-ready. Highly useful features, such as event checks and the ability to set the message sender, will be available in Motsu
with the release of Stylus 0.7.0
.
We are passionate about open source and welcome contributions from the community! This crate is maintained on a best-effort basis, as we use it extensively in our internal tests.
If you have ideas, suggestions, or feedback, feel free to open an issue or submit a PR. You can also check out the existing issues and get involved.