As a blockchain developer experienced in building across multiple chains and working with various smart contract languages, I’ve been captivated by the ecosystem’s growing embrace of Rust and Rust-inspired languages. From Solana’s native Rust support to emerging languages like Sway on Fuel, Move on Sui and Aptos, and Fe on Ethereum, a clear trend is taking shape—and it’s wearing a crab badge. 🦀
This shift highlights Rust’s rising dominance in the blockchain space. Major platforms have chosen Rust for their core infrastructure, benefiting from its performance, robust memory safety guarantees, and zero-cost abstractions. The abundance of skilled Rust developers has further fueled this adoption. Rust is being used for both core infrastructure and smart contracts in new projects, and established chains are increasingly looking to add support for it to strengthen their ecosystems.
And this is where Stylus comes in—it eliminates the need for Rust-inspired approximations and allows you to build directly with Rust itself.
Stylus is an upgrade to the Arbitrum Nitro tech stack, adding a second virtual machine that runs WebAssembly (WASM) instead of the Ethereum Virtual Machine (EVM). This allows developers to write smart contracts in languages like Rust, C, and C++, which compile to WASM. These languages are more efficient for compute-heavy tasks, resulting in faster execution and lower gas fees compared to Solidity. Developers can take advantage of existing libraries and tools from these languages to build smart contracts with greater efficiency.
Stylus is fully compatible with Solidity, meaning Solidity and WASM contracts can interact with each other. Developers can use Stylus to optimize specific parts of an application by replacing only the compute-heavy components with WASM contracts, while the rest of the application can remain in Solidity. This allows for fine-tuned performance improvements without having to rewrite entire dApps. Stylus is especially beneficial for memory and computation-intensive applications, such as complex DeFi protocols, zero-knowledge proofs and on-chain games.
To put Stylus through its paces, I decided to implement solutions to Advent of Code challenges as smart contracts. Not because the problems demand blockchain (they definitely don't 😄), but because coding challenges provide an excellent testbed for exploring a new technology's capabilities, limitations, and performance characteristics.
In case you haven’t heard of it, Advent of Code is an annual series of coding puzzles released each December. It’s a fun way to challenge yourself and improve your problem-solving skills. The code for this project is open source and available on GitHub.
Getting started with Stylus development required several tools, most of which I already had in my toolkit: the Rust toolchain, Docker, and Foundry. The only new component I needed was the Nitro devnode, which I set up using the scripts from the nitro-devnode repository.
With the environment ready, I installed cargo-stylus using cargo install cargo-stylus
and created my first project with cargo stylus new <PROJECT_NAME>
. This generated a project based on the Stylus hello world template, which implements Foundry's Counter example. I verified everything was working correctly by testing essential commands:
cargo stylus check
for dry-running the project, compiling to WASM, and verifying deployability
cargo stylus export-abi
for generating the contract's ABI
cargo stylus deploy
for deployment
I successfully verified the deployed contracts by calling them using Foundry cast.
While the initial setup went smoothly, I encountered some interesting challenges when implementing tests and converting the project to a monorepo structure.
For testing, I utilized TucksonDev's e2e-lib, a simplified fork of OpenZeppelin's e2e library designed for testing Stylus contracts. The first issue I encountered involved the console!()
macro, which wasn’t working during tests. Strangely, it worked fine when deploying contracts and interacting with them through Foundry cast, showing logs in the sequencer’s Docker container. The solution was straightforward once discovered: I needed to enable the debug feature in cargo dependencies by updating stylus-sdk = "0.6.0"
to stylus-sdk = { version = "0.6.0", features = ["debug"] }
.
The transition to a monorepo setup involved creating a Cargo workspace and organizing files. While this was relatively straightforward, both the cargo stylus check
and cargo stylus deploy
commands started throwing errors. I found inspiration in solving the issues in OpenZeppelin's Rust Contract repository, adapting their approach to create custom scripts for handling specific WASM files. Here's the resulting check-wasm.sh
script:
#!/bin/bash
# Adapted code from `OpenZeppelin/rust-contracts-stylus`
# Ref: https://github.com/OpenZeppelin/rust-contracts-stylus/blob/main/scripts/check-wasm.sh
# Function for checking contract wasm binary by crate name
check_wasm () {
local CONTRACT_CRATE_NAME=$1
local CONTRACT_BIN_NAME="${CONTRACT_CRATE_NAME//-/_}.wasm"
echo
echo "Checking contract $CONTRACT_CRATE_NAME"
cargo stylus check --wasm-file ./target/wasm32-unknown-unknown/release/"$CONTRACT_BIN_NAME"
}
# Retrieve all alphanumeric contract's crate names in `./contracts/solutions` directory.
get_solution_crate_names () {
find ./contracts/solutions -maxdepth 2 -type f -name "Cargo.toml" | xargs grep 'name = ' | grep -oE '".*"' | tr -d "'\""
}
# Build all projects
cargo build --release --target wasm32-unknown-unknown
# Ceck all projects
for CRATE_NAME in $(get_solution_crate_names)
do
check_wasm "$CRATE_NAME"
done
Initially, I started with a single smart contract containing all the Advent of Code solutions. However, by Day 5, the compressed size of the WASM smart contract exceeded the 24KB limit. To address this, I migrated to a more modular architecture and optimized contract size by:
Using #![no_std]
and instead importing needed data structures from external crates like alloc
and hashbrown
.
Split solutions into separate files, with each file dedicated to solving its own specific puzzle
The final architecture consists of three main components:
An orchestrator smart contract:
Acts as a proxy, tracking the addresses of deployed solution contracts for each day and routing calls appropriately. It also manages ownership and initialization status. Here's its storage structure:
#[storage]
#[entrypoint]
pub struct Orchestrator {
initialized: StorageBool,
owner: StorageAddress,
day_to_solution: StorageMap<u32, StorageAddress>,
}
The orchestrator's key function is solve
, which handles solving specific Advent of Code puzzles. Users provide the day, part, and input data, and the orchestrator determines the correct contract and method to invoke.
pub fn solve(&mut self, day: u32, part: u32, input: String) -> i64
A trait (or interface):
Defines the external API for solution contracts.
pub trait Solution {
fn solvepart1(&self, input: String) -> i64;
fn solvepart2(&self, input: String) -> i64;
}
One smart contract per puzzle:
Each solution contract is minimal, with no storage and two external methods:
solvepart_1
solvepart_2
Each method corresponds to one of the two parts of an Advent of Code puzzle.
This architecture allowed for scalability and ensured that each puzzle solution remained within the size constraints. The modular approach improved maintainability, making it easier to debug and update individual solutions without affecting others. Perhaps most importantly, it enabled independent testing and deployment of specific solutions, eliminating the need to redeploy everything when making changes to a single puzzle's implementation. Solutions to all puzzles can be found here.
Throughout the development process, I made the following noteworthy observations:
1. Large Binary Sizes: Some external crates, like regex, can significantly inflate the compiled WASM size. Adding regex pushed my contract size over 100KB, far exceeding the 24KB limit. This shows the importance of carefully choosing lightweight dependencies or creating custom implementations when needed.
2. Gas Limit Issues: Code that runs fast in native Rust can still exceed the gas limit in smart contracts. Profiling and debugging are essential to identify gas-heavy operations and optimize the most expensive parts, including not just computationally intensive tasks but also memory-intensive ones, to stay within the limits.
Using Arbitrum Stylus to solve Advent of Code challenges proved to be an excellent learning experience. While using smart contracts for solving coding puzzles might seem unconventional and overkill, it helped me explore both Stylus's capabilities and its current limitations. The project highlighted important considerations for Rust smart contract development, from managing contract sizes to optimizing for gas efficiency. As Stylus continues to mature, it offers an exciting option for developers looking to build compute-intensive applications on Arbitrum using Rust's powerful capabilities.
Website | Discord | LinkedIn | Twitter/X | GitHub | E-Mail
Written by Martin Domajnko, CTO