Mastering CosmWasm Multi-Test: Easily Test & Deploy Rust Smart Contract Apps

The CosmWasm Multi-Test package includes the features developers need to test nearly everything in multi-contract applications.

Peter Keay
Obi.Money

--

Multi-contract architectures are increasingly essential, even for simple blockchain applications.

But testing multi-contract dapps can be difficult.

  • Unit tests using cosmwasm_std::testing, as important as they are, can’t do multi-contract interactions.
  • Local machine test networks, as useful as they are, have demanding setup and aren’t always appropriate for CI/CD.
  • Testing on testnets or mainnets requires a long process of uploading and instantiating or migrating each and every contract before tests can be started.

Here I’ll show you how to use cw-multi-test to easily write multi-contract tests, including migration tests and time lock tests. These tests can be seamlessly included not only in your local cargo test workflow but also as a part of your Github Actions or other CI/CD solution.

Note: With Obi Universe, multi-test will even be usable to test smart contracts written on other Rust/WASM-supporting platforms, such as Solana, Near, Arbitrum’s Stylus, and perhaps even eWASM on Ethereum, if that is ever adopted. But for now, CosmWasm is the only focus of this article.

Just in case you’re unfamiliar with them, I’ll be highlighting some terms throughout that most readers will know, like this:

multi-test:cw-multi-test, which allows local testing of multiple CosmWasm smart contracts. The tester can handle bank functions, block info simulation, and multi-contract interactions, including replies and migrates.

Sample App Overview

We’ll create a mock App and interact generally it as though it’s a blockchain network: deploying and interacting with some convenientContractWrapper objects to it, using its Executor.

Although there will be no consensus engine running and no blocks produced, we can do almost any multi-contract interactions we need — including multiple messages, replies, and migrations.

Our sample app will be a simple contract factory: a smart contract which can instantiate other smart contracts. We will:

  1. Create a factory contract which knows a settable code ID which is used to instantiate new contracts.
  2. Instantiate a child contract with the code ID.
  3. Check the newly created child contract address. reply() saves the child contract address in the factory contract state for future use.

reply: a contract called by another contract using a SubMsg (docs) can send a Reply (more docs) back to it. The first contract must have a reply() entry point function in order to receive this reply, which the SubMsg specifies will happen ReplyOn::Error, ReplyOn::Success, or ReplyOn::Always. This way, the first contract can continue execution, based on the contents of the reply sent back by the second contract.

Once we’re done, we’ll know how to use multi-test to set up multiple contracts, send a multi-contract execute message, and query a contract. Then, we’ll look at mocking block times and native asset mints & sends in multi-test.

Setting Up: Some Code to Get Started

For you to easily follow along, I’ve created a Github repository:

If you’re unfamiliar with developing Rust smart contracts, or don’t have your environment set up for CosmWasm development, run through some basic CosmWasm docs such as the CosmWasm Book:

On the main branch in the cosmwasm-multitester-example repo, your Cargo.toml is already included, and the dependencies are imported into tests/src/tests_integration.rs, but none of the actual test code has been added.

You’ll be working in this tests_integration.rs file.

The completed branch contains the test code we’ll be setting up, but I highly recommend you work on main. You can compare with completed later if needed.

You can view the code for the simple contracts multi-test we’ll be deploying and interacting with in the contracts folder. Don’t modify any of the contracts themselves — unless you’d like to go beyond this tutorial and play around with their functionality on your own.

Setting up the Mock App

First, we need to set up a mock application, which can simulate contract state, environment, and execution.

//tests_integration.rs
use cw_multi_test::{App, Contract, ContractWrapper, Executor};

#[test]
fn integration_test() {
let mut router = App::default();
}

This will work, but it’s customary to create a mock_app() helper function for ease of use across multiple tests (and potentially with some handling code inside):

use cw_multi_test::{App, Contract, ContractWrapper, Executor};

#[allow(dead_code)]
fn mock_app() -> App {
App::default()
}

#[test]
fn integration_test() {
let mut router = mock_app();
}

Setting up the Mock Smart Contracts

In our Cargo.toml, we’ve imported the code for our contracts from the other crates in our workspace:

...
[dependencies]
factory = { path = "../contracts/factory" }
child = { path = "../contracts/child" }

You shouldn’t need to change any files. This allows us to directly access the contract code within our tests, e.g. factory::msg::InstantiateMsg or child::contract::execute.

Creating ContractWrappers

For each contract, we need to create a ContractWrapperfor use in our test file. Here’s what this would look like for child:

use cosmwasm_std::Empty;

// outside of the test
#[allow(dead_code)]
fn child_contract() -> Box<dyn Contract<Empty>> {
let contract = ContractWrapper::new_with_empty(
child::contract::execute,
child::contract::instantiate,
child::contract::query,
)
// .with_reply(child::contract::reply);
.with_migrate(child::contract::migrate);
Box::new(contract)
}

#[test]
...

Box: You might not be familiar with Box, which lets you point to heap data rather than stack data. Here, it’s used to return a value that doesn’t have a known size at compilation time. In others words, you can’t just return a naked dyn Contract, since the size of anything dyn is determined at runtime, not compilation time. You may have encountered compiler errors referring to ?Sized which could be addressed using this method. (Box also has many other uses, inside and outside of contract programming, such as recursive lists and lifetime management. You should understand it. Read more here.)

Notice the required additions in order to enable reply()and migrate(). You may not need these; for our tester as demonstrated in the start repo, the factory contract doesn’t even have a migrate function — and the child doesn’t have a reply, so we must omit .with_migrate or .with_reply, respectively, in order for the code to compile.

Storing Contract Code

Once we’ve established our wrapped contract, we need to store its code.

code: on CosmWasm chains, code is a WASM contract that has been uploaded to chain. It has a code id (u64, a 64-bit positive integer) and can be instantiated any number of times. On many chains, anyone can deploy code. But some chains, such as Osmosis, are permissioned —usually, only a governance vote can deploy new mainnet code on these chains.

Storing the code will return a code ID, just like when we upload contracts to networks from web interfaces or command lines.

let child_codeid = app.store_code(child_contract());

Instantiating a Contract

Now we can instantiate this contract in a test however many times we want:

use cosmwasm_std::{Addr, Empty};

let contract_owner = "OWNER";
let child_contract_addr = router
.instantiate_contract(
child_codeid,
Addr::unchecked(contract_owner),
&InstantiateMsg {},
&[], // funds
"Old Contract Code", // label
None, // code admin (for migration)
)
.unwrap();

Directly instantiating the child contract, however, doesn’t fit the original plan. Instead, we should:

  1. instantiate the factory
  2. send the factory a NewContract execute message so that it creates an instance of child

Step 1 requires just a little bit of rewriting. Here’s the full test code so far, but instantiating factory instead of child:

use cw_multi_test::{App, Contract, ContractWrapper, Executor};
use cosmwasm_std::{Addr, Empty};

#[allow(dead_code)]
fn mock_app() -> App {
App::default()
}

#[allow(dead_code)]
fn child_contract() -> Box<dyn Contract<Empty>> {
let contract = ContractWrapper::new_with_empty(
child::contract::execute,
child::contract::instantiate,
child::contract::query,
)
.with_migrate(child::contract::migrate);
Box::new(contract)
}

#[allow(dead_code)]
fn factory_contract() -> Box<dyn Contract<Empty>> {
let contract = ContractWrapper::new_with_empty(
factory::contract::execute,
factory::contract::instantiate,
factory::contract::query,
)
.with_reply(factory::contract::reply);
Box::new(contract)
}

#[test]
fn integration_test() {
println!("Running integration test...");
let mut router = mock_app();

let child_codeid = router.store_code(child_contract());

let factory_codeid = router.store_code(factory_contract());
let factory_owner = "OWNER";
let factory_contract_addr = router
.instantiate_contract(
factory_codeid,
Addr::unchecked(factory_owner),
&factory::msg::InstantiateMsg { child_codeid: child_codeid },
&[], // funds
"Contract Factory", // label
None, // code admin (for migration)
)
.unwrap();
}

Now you should be able to successfully run this integration test! Hit >Run Test in your VS Code window or type cargo test into a terminal from either the workspace root or from /tests/.

Notice that there is nothing multi-contract happening yet; we’re just instantiating one of the contracts. However, we’re doing this in the context of multi-test, so we’re now all set up to try multi-contract interactions.

Working with our Contracts

Sending an ExecuteMsg to a Contract

Now the child code is stored and the factory is aware of its code ID, so we can ask the factory to instantiate a contract:

let execute_msg = factory::msg::ExecuteMsg::NewContract {};
let res = router
.execute_contract(
Addr::unchecked(factory_owner),
factory_contract_addr.clone(), // clone since we'll use it again
&execute_msg,
&[], // funds
)
.unwrap();

We can inspect the last Event in res to get the resulting contract address:

  let instantiation_event = res.events[res.events.len() - 1].clone();
println!("Instantiated code id {} to address {}",
instantiation_event.attributes[1].value,
instantiation_event.attributes[0].value
);

Resulting in the following output when we run cargo test -- --show-output:

running 1 test
Running integration test...
Instantiated code id 1 to address contract1
test tests_integration::integration_test ... ok

In our multi-test context, our factory contract was given code ID 0 and address contract0 — code and contracts are mocked using sequential zero-indexed code IDs and contract addresses.

In this case, our factory contract also stores the address when the newly instantiated contract replies, so let’s query to make sure this happened correctly.

Note that this reply wouldn’t work in multi-test if we neglected to add .with_reply() to our ContractWrapper.

Querying a Contract

We can now query the factory to make sure it knows it has instantiated a child contract and has stored its address:

let query_msg = factory::msg::QueryMsg::Children {};
let query_response: factory::msg::ChildrenResponse = router
.wrap()
.query_wasm_smart(
factory_contract_addr,
&query_msg,
)
.unwrap();
assert_eq!(query_response.children.len(), 1);
assert_eq!(query_response.children[0], "contract1".to_string());

Advancing Block Time

Many projects include staking or time lock components and will require your tests to advance time, or block height, in order to check that your contracts are working correctly.

The multi-test App (which we’ve been using as the instance router) makes this easy by providing block_info(). For example, we can set the block height to 12345 and advance block time by 1 day:

let old_block_info = router.block_info();
router.set_block(BlockInfo {
height: 12345u64,
time: Timestamp::from_seconds(old_block_info.time.seconds() + 74056),
chain_id: old_block_info.chain_id,
});

Network Assets

Network Asset: also native asset, handled by the Bank CosmosSDK module. This is in contrast to a token handled by a contract, usually cw20. Sending a native asset requires a BankMsg, while a cw20 will be sent using WasmMsg::Execute with either a Cw20ExecuteMsg::Transfer (which does not trigger receiving smart contracts) or a Cw20ExecuteMsg::Send (which does).

Sending around network assets is another common need in tests. On a live network, you can’t use SudoMsg to do anything, but you’re free to do it in local contexts.

The following code:

  • uses SudoMsg::Bank to mint 1.1 MULTI tokens directly to alice
  • sends 0.5 of them to bob
use cosmwasm_std::{coin, Coin, Uint128};

/// thanks William. This code first seen at
// https://www.noveltech.dev/cw_multi_test_coin
pub fn mint_native(app: &mut App, recipient: String, denom: String, amount: u128) {
app.sudo(cw_multi_test::SudoMsg::Bank(
cw_multi_test::BankSudo::Mint {
to_address: recipient,
amount: vec![coin(amount, denom)],
},
))
.unwrap();
}

// mint 1.1 MULTI to alice
mint_native(
&mut router,
"alice".to_string(),
"umulti".to_string(),
1_100_000u128,
);

// send 0.5 MULTI from alice to bob
router
.send_tokens(
"alice".to_string(), // sender
"bob".to_string(), // recipient
&[coin(500_000u128, "umulti"],
)
.unwrap();

In order to send cw20 assets around, you can simply deploy a cw20 contract and interact with it just as we did with the factory and child contracts in this tutorial.

Continuing The Project

If you’re interested in working with multitest, you should try to add some additional functionality or tests yourself, using the block time and sudo mint capabilities just described. Here are some ideas:

  1. Add an execute message to the factory contract which updates the code ID.
  2. Add an execute message to the factory contract which migrates all of the children to the known code ID if it is greater than the given child’s current code ID.
  3. Add integration tests for #1 and #2. Ensure that a child’s code ID is “old” and then, after being updated by asking the factory to migrate children, ensure the child’s code ID has updated to “new.”
  4. Enforce a delay: a child can only be instantiated by the factory if some time has passed since the last instantiation.
  5. Add a fee that must be paid as funds when the factory is called. Your test should send a user some network assets and then confirm that NewContract only works if the proper funds are attached to the transaction.
  6. Add a cw20 token contract to the workspace and change the workflow for paying the factory to run NewContract so that it automatically does so when receiving the correct number of cw20 tokens. Use Cw20ReceiveMsg to accomplish this.

migrate: an instantiated contract can be updated to a new code ID by calling its migrate() entry point, with a MigrateMsg {} which can have any parameters needed to complete the upgrade. This entry point and the MigrateMsg must be defined in the contract, or it will not be migrateable. In addition, the contract must have an address that is code admin, set when it is instantiated, and which has migration authority.

What Multi-Test Won’t Catch

Don’t make multi-test the only step between your development and your deployment: on-chain tests are necessary as well.

Among other things:

  • multi-test won’t catch errors like floats in the compiled WASM
  • Mock contracts you’ve added to simulate interactions with deployed mainnet contracts might have some schema differences
  • Execution may be too involved or expensive
  • multi-test doesn’t mock IBC (inter-blockchain communication)

Note on Multi-Test Code Admin limitations:

multi-test currently supports migrate(), and setting code admin when instantiating, as shown above.

However, it does NOT support the UpdateAdmin message. Unfortunately, then, you cannot test flows which involve updating code admins (migration authorities) from their original accounts to new accounts. Be sure to set this value correctly on instantiation.

One workaround here is to set a contract’s code admin to itself and have the contract expose a WrappedMigrate message which can be called by an address. That address field can then be updated as needed. This approach has numerous other benefits as well…

Want more Rust and smart contract dev content? Clap for this article and follow me and Obi on Twitter, and I’ll keep it coming!

Retweet this article tweet:

Peter Keay is co-founder and head of technology at Obi.money, which is building multikey account abstraction SDKs for any web or blockchain environments.

--

--

Peter Keay
Obi.Money

Rust/C++/WASM smart contract dev || Creative || Writer