Stylus Saturdays, 7th September 2024
We made it! Congratulations to Offchain Labs for getting us to mainnet successfully!
Fantastic work to everyone at the Offchain Labs team for such a successful and frictionless launch! We’re all super excited that Arbitrum Stylus is now on mainnet. Returning to a regular groove this week, we’re also experimenting with a technical piece at the end of the article: how to do coverage testing of Stylus contracts!
Arbitrum Stylus is a fantastic new technology for building on the EVM: WASM-powered smart contracts that are orders of magnitude cheaper and more expressive than their Solidity counterparts. This means a hugely cheaper, safer, and more accessible Arbitrum for everyone! Stylus contracts are compatible with the EVM day 1: that also means no friction with the existing ecosystem.
You can learn more about Stylus here: Arbitrum Stylus
At Superposition, we’re super excited to be building with this platform and technology, and we want to share with the world this emerging ecosystem. Superposition is building the first defi-native chain that pays you to use it!
Make sure to mint your Stylus NFT at https://www.mintstylus.xyz/!
Week 3: Fairblock
Fairblock is a programmable privacy layer, originally based on Cosmos. They have a suite of privacy protecting tools including private order execution with their MPC technology Fairyring. This is their private Coincidence Of Wants swap:
This also have a private on-chain swap based on a modified Uniswap V2 on an orbit chain. Fairblock is leveraging Stylus for their new developments! They have several cool products involving on-chain privacy, an on-chain murder mystery, a private voting platform, and verifiable randomness.
They’ve announced a testnet at https://medium.com/@0xfairblock/fairblock-public-testnet-is-now-live-a487f97ea3e0!
Their Discord lives at https://discord.gg/5ApWZhcKsX.
Other project updates
Congratulations to Renegade for a successful mainnet launch!
Check out receiving a subsidised audit from the foundation and OpenZeppelin: https://x.com/OpenZeppelin/status/1828894414007349291.
How to do testing of your Stylus dApp?
There are a few ways you can do testing of your code!
Testing with unit tests (manually)
We have to refresh some background here to understand how this works. Your contracts are compiling to the architecture wasm32
when you compile with cargo build --target wasm32-unknown-unknown
. But when you run tests with cargo test
(without the target flag setting the wasm target), they’re run with your native environment (x86_64
for most people)!
Using the test
macro and small functions, we can simple build unit tests:
#[test]
fn test_something() {
assert_eq!(123, 123);
}
This is simple, and it can be useful for capturing the result of some maths for example, but we can’t capture end-to-end tests with functions that touch the state of the contract.
End-to-end testing with Rust in Stylus
To do this we need to implement some of the functions that Stylus expects to be available without garbling their symbols. In the Superposition contracts for the contract “Leo” (liquidity mining code), this is a simple implementation: https://github.com/fluidity-money/long.so/blob/development/pkg/leo/src/host.rs#L39-L102.
These functions will need to be implemented, regardless of whether you use logging (of events), working with the block timestamp, etc. They work with pointers:
#[no_mangle]
pub unsafe extern "C" fn storage_store_bytes32(key: *const u8, value: *const u8)
#[no_mangle]
pub unsafe extern "C" fn storage_cache_bytes32(key: *const u8, value: *const u8)
#[no_mangle]
pub unsafe extern "C" fn storage_load_bytes32(key: *const u8, out: *mut u8)
#[no_mangle]
pub unsafe fn storage_flush_cache(_clear: bool)
These functions need to do something with the storage! In the Leo codebase, we use a hashmap that’s created on a thread local basis. This is adjusted to have a function to clear the storage:
thread_local! {
pub static STORAGE: RefCell<HashMap<Word, Word>> = RefCell::new(HashMap::new());
}
#[no_mangle]
pub unsafe extern "C" fn storage_store_bytes32(key: *const u8, value: *const u8) {
let (key, value) = unsafe {
// SAFETY - Stylus insists these will both be valid words
(read_word(key), read_word(value))
};
STORAGE.with(|storage| storage.borrow_mut().insert(key, value));
}
#[no_mangle]
pub unsafe extern "C" fn storage_cache_bytes32(key: *const u8, value: *const u8) {
// We don't need to implement caching since there's no storage overhead
// in testing.
storage_store_bytes32(key, value);
}
#[no_mangle]
pub unsafe extern "C" fn storage_flush_cache(v: bool) {
// Clear the storage hashmap!
STORAGE.with(|storage| storage.borrow_mut().clear());
}
#[no_mangle]
pub unsafe extern "C" fn storage_load_bytes32(key: *const u8, out: *mut u8) {
// SAFETY - Stylus guarantees that this is available.
let key = unsafe { read_word(key) };
let value = STORAGE.with(|storage| {
storage
.borrow()
.get(&key)
.map(Word::to_owned)
.unwrap_or_default()
});
unsafe { write_word(out, value) };
}
So in practice, you might have a file named host.rs
that does this, and a function that does some work to clear the local state in advance of running a specific test.
For implementing some code that supports setting the msg::sender
or block.timestamp.
you could refer to the Leo code to see how we do it.
#[test]
fn test_end_to_end() {
StorageCache::clear(); // Make sure we're clear.
// Create the contract.
let contract = unsafe {
<Contract as stylus_sdk::storage::StorageType>::new(i, v)
};
// Now we can use it like we normally would!
contract.do_something().unwrap();
}
For an example of how we do it in practice, check out the with_storage
function in the Leo codebase: https://github.com/fluidity-money/long.so/blob/development/pkg/leo/src/host.rs#L125-L142.
Testing with OpenZeppelin (unit testing)
OZ’s testing suite located at https://github.com/OpenZeppelin/rust-contracts-stylus/tree/main/lib/motsu and https://github.com/OpenZeppelin/rust-contracts-stylus/tree/main/lib/e2e is respectively unit testing (with setup similar to the above), and end to end testing using the Nitro node.
Comparatively to the above, testing with OpenZeppelin Motsu is a lot simpler. The crate does the local definitions in the way that we explained above. It should be easy to add contract setup features to instantiate your contract storage on demand. Defining a unit test is as simple as using a macro like below (this is ripped directly from their example):
#[cfg(test)]
mod tests {
use contracts::token::erc20::Erc20;
#[motsu::test]
fn reads_balance(contract: Erc20) {
let balance = contract.balance_of(Address::ZERO); // Access storage.
assert_eq!(balance, U256::ZERO);
}
}
It’s great in that you can use a few pre-made libraries for things like ERC20! Working with OpenZeppelin’s library for this instead of redoing it yourself is a lot simpler.
Testing with OpenZeppelin (end to end)
OZ’s testing suite works with the local Nitro instance for end-to-end testing. An example in their documentation is as follows:
#[e2e::test]
async fn accounts_are_funded(alice: Account) -> eyre::Result<()> {
let balance = alice.wallet.get_balance(alice.address()).await?;
let expected = parse_ether("10")?;
assert_eq!(expected, balance);
Ok(())
}
With native accounting available for ERC20, you can see how this is easier to test with.
Stylus resources
In no particular order:
And that’s it for this week!
If your project has any updates you’d like to share, or you would like to request that we monitor your project’s developments to include you in the weekly newsletter, please reach out to @baygeeth on X!
Stylus Saturdays is powered by Superposition, the first defi-native layer 3 that pays you to use it. Try out the incentivised testnet at https://superposition.so.