Multitests
How to write Multitests for Cosmwasm Smart Contracts
When working with Secret Network, official support for multi-tests is not currently provided. As a result, when porting over a standard CosmWasm smart contract, it's advisable to exclude the
integration_tests.rs
file from your project. This page of documentation explains how to write standard CosmWasm multitests, but this is not yet supported on Secret Network. cw_multi_test
is a Rust library that provides utilities for writing integration tests for CosmWasm smart contracts. According to the rust docs:"Multitest is a design to simulate a blockchain environment in pure Rust. This allows us to run unit tests that involve contract -> contract, and contract -> bank interactions. This is not intended to be a full blockchain app but to simulate the Cosmos SDK x/wasm module close enough to gain confidence in multi-contract deployments before testing them on a live blockchain."
To use
cw_multi_test
, you need to add it to your project's Cargo.toml
file as a dependency:[dev-dependencies]
cw-multi-test = "0.13.4"
For this tutorial, we are analyzing a multitest that is part of the cw3-fixed-multisig CosmWasm repo. This contract is simply a multisig contract with a fixed set of addresses created upon instantiation, and we are testing whether or not the multisig contract can execute a cw20 contract successfully.
The terms "Integration test" and "multitest" are used interchangeably.
This is a Rust integration test for a cosmwasm smart contract that demonstrates the use of the
cw_multi_test
crate.The test sets up two contracts: (1) a CW3 multisig contract and (2) a CW20 token contract, and verifies that the multisig contract can control CW20 admin actions. The test case first deploys the CW3 multisig contract with three voters and a voting threshold of 2 (this just means that there are 3 wallet addresses that can vote and they each have equal voting power), and then deploys the CW20 token contract with the multisig contract as the minter.
It then creates a proposal to mint new tokens, which is voted on and executed by the multisig contract. Finally, it queries the balance of the mint recipient to verify that the tokens were successfully minted. The test case demonstrates how to use
cw_multi_test
to create a mock app, deploy contracts, and execute transactions with multiple contracts. Let's examine it line-by-line: #[cfg(test)]
use cosmwasm_std::{to_binary, Addr, Empty, Uint128, WasmMsg};
use cw20::{BalanceResponse, MinterResponse};
use cw20_base::msg::QueryMsg;
use cw3::Vote;
use cw_multi_test::{App, Contract, ContractWrapper, Executor};
use cw_utils::{Duration, Threshold};
use crate::contract::{execute, instantiate, query};
use crate::msg::{ExecuteMsg, InstantiateMsg, Voter};
fn mock_app() -> App {
App::default()
}
First, the test sets up a mock application and imports the contract dependencies. At the beginning of the test, an
App
object is created. App
is a core multitest entity representing the virtual blockchain on which we run our contracts.pub fn contract_cw3_fixed_multisig() -> Box<dyn Contract<Empty>> {
let contract = ContractWrapper::new(execute, instantiate, query);
Box::new(contract)
}
pub fn contract_cw20() -> Box<dyn Contract<Empty>> {
let contract = ContractWrapper::new(
cw20_base::contract::execute,
cw20_base::contract::instantiate,
cw20_base::contract::query,
);
Box::new(contract)
}
The above functions define a CW3 multisig smart contract instance, as well as a CW20 token smart contract instance.
Box::new(contract)
creates a new heap-allocated Box
that owns the contract
instance and returns it as a trait object. Here, the
contract
instance is created using the ContractWrapper::new()
method, which takes in three functions as arguments: execute
, instantiate
, and query
. These functions are defined in the contract
module and are used to implement the execute
, instantiate
, and query
methods of the Contract
trait.Now let's examine the
cw3_controls_cw20()
function:#[test]
fn cw3_controls_cw20() {
let mut router = mock_app();
}
mut router
initializes the router mock app.let cw3_id = router.store_code(contract_cw3_fixed_multisig());
cw3_id
stores the cw3 multisig contract code in the router and get its ID. let addr1 = Addr::unchecked("addr1");
let addr2 = Addr::unchecked("addr2");
let addr3 = Addr::unchecked("addr3");
addr1, addr2, addr3
are three arbitrary addresses to use as voters for the cw3 multisig account.let cw3_instantiate_msg = InstantiateMsg {
voters: vec![
Voter {
addr: addr1.to_string(),
weight: 1,
},
Voter {
addr: addr2.to_string(),
weight: 1,
},
Voter {
addr: addr3.to_string(),
weight: 1,
},
],
threshold: Threshold::AbsoluteCount { weight: 2 },
max_voting_period: Duration::Height(3),
};
cw3_instantiate_msg
creates an instantiation message for the cw3 multisig account. let multisig_addr = router
.instantiate_contract(
cw3_id,
addr1.clone(),
&cw3_instantiate_msg,
&[],
"Consortium",
None,
)
.unwrap();
multisig_addr
instantiates the cw3 multisig account and get its address. let cw20_id = router.store_code(contract_cw20());
let cw20_instantiate_msg = cw20_base::msg::InstantiateMsg {
name: "Consortium Token".parse().unwrap(),
symbol: "CST".parse().unwrap(),
decimals: 6,
initial_balances: vec![],
mint: Some(MinterResponse {
minter: multisig_addr.to_string(),
cap: None,
}),
marketing: None,
};
let cw20_addr = router
.instantiate_contract(
cw20_id,
multisig_addr.clone(),
&cw20_instantiate_msg,
&[],
"Consortium",
None,
)
.unwrap();
cw20_id
stores the cw20 contract code in the router and get its ID. cw20_instantiate_msg
creates an instantiation message for the cw20 contract, setting the multisig account as the minter. cw20_addr
instantiates the cw20 contract and get its address. let mint_recipient = Addr::unchecked("recipient");
let mint_amount = Uint128::new(1000);
let cw20_mint_msg = cw20_base::msg::ExecuteMsg::Mint {
recipient: mint_recipient.to_string(),
amount: mint_amount,
};
let execute_mint_msg = WasmMsg::Execute {
contract_addr: cw20_addr.to_string(),
msg: to_binary(&cw20_mint_msg).unwrap(),
funds: vec![],
};
let propose_msg = ExecuteMsg::Propose {
title: "Mint tokens".to_string(),
description: "Need to mint tokens".to_string(),
msgs: vec![execute_mint_msg.into()],
latest: None,
};
router
.execute_contract
cw20_mint_msg
creates a message to mint some cw20 tokens according to a proposal result. execute_mint_msg
create an execute message to execute the cw20_mint_msg on the cw20 contract.propose_msg
creates a proposal message that includes the execute_mint_msg
and proposes it to the multisig account. // only 1 vote and msg mint fails
let execute_proposal_msg = ExecuteMsg::Execute { proposal_id: 1 };
// execute mint
router
.execute_contract(addr1, multisig_addr, &execute_proposal_msg, &[])
.unwrap();
// check the mint is successful
let cw20_balance_query = QueryMsg::Balance {
address: mint_recipient.to_string(),
};
let balance: BalanceResponse = router
.wrap()
.query_wasm_smart(&cw20_addr, &cw20_balance_query)
.unwrap();
// compare minted amount
assert_eq!(balance.balance, mint_amount);
Lastly, the function executes the proposal and checks that the mint was successful by querying the balance of the recipient's address. The line "assert_eq!(balance.balance, mint_amount)" is an assertion that checks whether the value of the
balance
variable is equal to the value of the mint_amount
variable.In the previous lines of code, the
balance
variable was assigned the value of the balance of the cw20
token held by the mint_recipient
address after the minting process. The mint_amount
variable was assigned the value of 1000
cw20
tokens which were minted in the process.Therefore, the assertion is checking whether the
cw20
tokens were minted and the balance
of cw20
tokens held by the mint_recipient
address is equal to the mint_amount
that was minted. If the assertion fails, the test will panic.
Last modified 1mo ago