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.
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:
- Create a factory contract which knows a settable code ID which is used to instantiate new contracts.
- Instantiate a child contract with the code ID.
- 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 happenReplyOn::Error
,ReplyOn::Success
, orReplyOn::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 ContractWrapper
for 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 nakeddyn Contract
, since the size of anythingdyn
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:
- instantiate the
factory
- send the
factory
aNewContract
execute message so that it creates an instance ofchild
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 usingWasmMsg::Execute
with either aCw20ExecuteMsg::Transfer
(which does not trigger receiving smart contracts) or aCw20ExecuteMsg::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 toalice
- 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:
- Add an execute message to the
factory
contract which updates the code ID. - Add an execute message to the
factory
contract which migrates all of thechildren
to the known code ID if it is greater than the given child’s current code ID. - 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.” - Enforce a delay: a
child
can only be instantiated by thefactory
if some time has passed since the last instantiation. - Add a fee that must be paid as
funds
when thefactory
is called. Your test should send a user some network assets and then confirm thatNewContract
only works if the properfunds
are attached to the transaction. - Add a cw20 token contract to the workspace and change the workflow for paying the
factory
to runNewContract
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 aMigrateMsg {}
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.