Add Tests To Your Stylus Contracts

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

Testing your Stylus project with Motsu

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 open for contributions

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.