Testing Arbitrum Stylus Smart Contracts with Motsu

Table of contents

Introduction

As Ethereum scaling solutions mature, Arbitrum Stylus stands out as a fascinating innovation that lets developers write smart contracts in Rust. This offers significant performance improvements and brings Rust's robust type system and memory safety guarantees to the blockchain world.

But as any experienced smart contract developer knows, thorough testing is non-negotiable. That's where Motsu comes in – a testing framework designed specifically for Stylus contracts that feels both familiar to Rust developers and conceptually similar to tools like Hardhat and Foundry that Solidity developers know well.

In this tutorial, we'll explore how to effectively test your Stylus contracts using Motsu. If you're coming from a Solidity background with experience in tools like Foundry or Hardhat, you'll find many parallels that will help you get up to speed quickly.

You can find the complete working project with all examples in our GitHub repository here: https://github.com/0xNeshi/motsu-tutorial

What Makes Motsu Special?

Motsu addresses a core challenge in testing Stylus smart contracts: simulating the blockchain environment. Just as Hardhat provides a JavaScript environment for testing Solidity contracts, and Foundry provides a Solidity-native testing experience, Motsu delivers a pure Rust testing experience for Stylus contracts by mocking the vm affordances. Unlike Hardhat or Foundry, which spin up lightweight blockchain nodes to execute tests, Motsu takes a different approach by directly intercepting and mocking Stylus host functions at the Wasm level. This enables fast, isolated tests without requiring a full blockchain runtime, making it ideal for unit testing Stylus contracts purely in Rust.

The name "Motsu" (持つ, Japanese for "to hold") cleverly references "holding a stylus in our hand" – a fitting metaphor for a tool that gives you control over your Stylus contract tests.

Setting Up Your Project

Before diving into Motsu, make sure your project is properly configured. Below are the dependencies that we’re going to need for this project:

Arbitrum Stylus OpenZeppelin Motsu Code Snippet 1Let’s add the contract in our src/lib.rs:Arbitrum Stylus OpenZeppelin Motsu Code Snippet 2

Writing Your First Test

Let's start with a simple example. If you've written tests in Rust before, the structure will look familiar, with one key difference: instead of using #[test], we use #[motsu::test].

Here's a basic test for our Vault contract:Arbitrum Stylus OpenZeppelin Motsu Code Snippet 3Let's break down what's happening here:

  1. The #[motsu::test] attribute transforms your test function to provide access to the VM environment
  2. Parameters to the test function are automatically injected:
    • contract: Contract<Vault> creates an instance of your contract
    • alice: Address creates a test address

This is conceptually similar to how Foundry's setUp() function prepares your test environment, but in a more Rust-idiomatic way through function parameters.

Accounts and Addresses

Motsu provides two types for representing blockchain accounts:

  1. Address - A simple Ethereum address (like Ethereum's address type)
  2. Account - An address with an associated private key for signing operations

You can use either as parameters in your test functions:Arbitrum Stylus OpenZeppelin Motsu Code Snippet 4Note that you need to add alloy-signer@0.11.1 to your [dev-dependencies] in Cargo.toml to be able to perform signing operations.

Unless you need to access the private key or the underlying signer in your tests we recommend using Address as it is more lightweight.

Deterministic Accounts with Tags

When debugging tests, having consistently named accounts can help track issues. Motsu provides a FromTag trait that is included in its prelude to create deterministic addresses from string identifiers, and this trait is what’s used to inject test parameters:Arbitrum Stylus OpenZeppelin Motsu Code Snippet 5

Interacting with Contracts

The core of Motsu testing revolves around the Contract<T> type, which represents a deployed instance of your smart contract.

Basic Interactions

To call functions on your contract:Arbitrum Stylus OpenZeppelin Motsu Code Snippet 6The sender() method sets the caller of the transaction, similar to Hardhat's connect() or Foundry's vm.prank().

Passing Value with Calls

Imagine we added a payable deposit function to our Vault that accepts the underlying gas token:Arbitrum Stylus OpenZeppelin Motsu Code Snippet 7For payable functions, you can send ETH using sender_and_value().Arbitrum Stylus OpenZeppelin Motsu Code Snippet 8

Testing Events

Smart contracts often emit events that you'll want to verify in your tests.

First, let’s update our deposit function to emit an event on a successful deposit:Arbitrum Stylus OpenZeppelin Motsu Code Snippet 9Motsu provides an elegant way to check emitted events:Arbitrum Stylus OpenZeppelin Motsu Code Snippet 10In case of a failing event assertion, Motsu provides well formatted panic messages with account/contract addresses substituted with appropriate tags, if tags were used to instantiate them.Arbitrum Stylus OpenZeppelin Motsu Code Snippet 11

Handling Reverts

Testing failure scenarios is just as important as testing successful operations. Let’s add a decrease_balance function to Vault that reverts with an error on balance underflow:Arbitrum Stylus OpenZeppelin Motsu Code Snippet 12Motsu provides several methods to handle reverts properly:Arbitrum Stylus OpenZeppelin Motsu Code Snippet 13The key methods for handling reverts are:

  • motsu_unwrap() - Unwraps the result or panics with error details
  • motsu_unwrap_err() - Expects an error and returns it, or panics if successful
  • motsu_expect() - Like unwrap() but with a custom message
  • motsu_expect_err() - Like unwrap_err() but with a custom message
  • motsu_res() - Reverts the transaction on error but returns the Result

These methods ensure that the contract state is properly reverted when transactions fail, similar to how real blockchain transactions behave. In case any of the above methods panic, the panic messages will be pretty-printed for clarity, with addresses being replaced with appropriate tags if that’s how they were instantiated.Arbitrum Stylus OpenZeppelin Motsu Code Snippet 14

Testing Contract-to-Contract Interactions

One of Motsu's powerful features is the ability to test interactions between multiple contracts. This is essential for complex DeFi applications or any system with multiple interacting components.

Let’s set up a simple proxy contract implementation:Arbitrum Stylus OpenZeppelin Motsu Code Snippet 15Motsu handles the internal contract-to-contract wiring, allowing us to easily test the above proxy:Arbitrum Stylus OpenZeppelin Motsu Code Snippet 16

Manipulating the VM Environment

Sometimes you need to simulate specific blockchain conditions. Motsu lets you modify certain environment variables like chain ID, which lets you simulate a chain fork.Arbitrum Stylus OpenZeppelin Motsu Code Snippet 17

Practical Example: Testing an ERC-20 Token

Let's put everything together by testing an ERC-20 token implementation.

We’ll inherit openzeppelin-stylus library’s ERC-20 implementation, so let’s add the necessary dependency to our Cargo.toml:Arbitrum Stylus OpenZeppelin Motsu Code Snippet 18We can now open src/lib.rs and implement our ERC-20 token together with a comprehensive transfer test:Arbitrum Stylus OpenZeppelin Motsu Code Snippet 19

Tips for Effective Testing

Based on the patterns we've explored, here are some best practices for testing with Motsu:

  1. Use deterministic accounts with tags for easier debugging
  2. Test both success and failure cases using the appropriate motsu_* functions
  3. Verify events to ensure your contract is communicating state changes correctly
  4. Test complex interactions between multiple contracts
  5. Check balances before and after operations that involve value transfers
  6. Use account types appropriately - Address for simple cases, Account when signing is needed
  7. Remember to implement TopLevelStorage for contracts without #[entrypoint]

Conclusion

Motsu provides a powerful, Rust-native testing environment for Arbitrum Stylus contracts. While conceptually similar to tools like Hardhat and Foundry, it leverages Rust's type system to provide a more integrated testing experience.

For developers coming from Solidity, the mental model is quite transferable - you're still working with contracts, addresses, and transactions. But Motsu's design takes advantage of Rust features to make tests more concise and easier to reason about.

As you continue developing with Stylus, keep exploring Motsu's capabilities. The time invested in writing thorough tests will pay dividends in the reliability and security of your smart contracts.

Happy testing!