CosmWasm MultiTest 2.1.0 is out!

Dariusz Depta
CosmWasm
Published in
10 min readJul 11, 2024
Photo by Robert Zunikoff on Unsplash

Recently, version 2.1.0 of CosmWasm MultiTest has been released! In the following sections, we explain in detail what has changed compared to the previous version.

Summary of major changes in CosmWasm MultiTest 2.1.0

  • removed all default feature flags,
  • removed all feature flags from cosmwasm-std dependency,
  • added staking, stargate and cosmwasm_x_y feature flags,
  • made functionaddr_make available during initialization,
  • removed validation of validator addresses,
  • enabled empty string attributes,
  • improved the unbonding.

🔩 Refactored feature flags

We have recognized the importance of giving users greater control over their testing environments in terms of dependency configuration. We decided — although it’s a breaking change (in the SemVer sense) — to remove all default features from MultiTest. Starting with version 2.1.0 of MultiTest, we will strive to keep the default features set empty.
As a consequence, users will have to specify precisely which CosmWasm version they need for simulating their chain in tests. Generally — since version 2.0.0 — MultiTest is designed for testing smart contracts based on CosmWasm 2.0 or later.

Specifying the following features enables all testing capabilities for smart contracts based on CosmWasm 2.0 or later:

[dev-dependencies]
cw-multi-test = { version = "2.1.0", features = ["cosmwasm_2_0", "staking", "stargate"] }

Another step towards flexibility was removing features from cosmwasm-std dependency. Now, cosmwasm-std/staking and cosmwasm-std/stargate features can be enabled using MultiTest features staking and stargate, respectively. The full list of MultiTest features since version 2.1.0 is shown below.

Summary of features in CosmWasm MultiTest 2.1.0

  • backtrace — enables backtrace feature in anyhow dependency,
  • staking — enables staking feature in cosmwasm-std dependency and enables staking module in MultiTest,
  • stargate — enables stargate feature in cosmwasm-std dependency and enables stargate module in MultiTest,
  • cosmwasm_1_1 — enables cosmwasm_1_1 feature in cosmwasm-std dependency,
  • cosmwasm_1_2 — enables cosmwasm_1_2 feature in cosmwasm-std dependency and cosmwasm_1_1 feature in MultiTest,
  • cosmwasm_1_3 — enables osmwasm_1_3 feature in cosmwasm-std dependency and cosmwasm_1_2 feature in MultiTest,
  • cosmwasm_1_4 — enables cosmwasm_1_4 feature in cosmwasm-std dependency and cosmwasm_1_3 feature in MultiTest,
  • cosmwasm_2_0 — enables cosmwasm_2_0 feature in cosmwasm-std dependency and cosmwasm_1_4 feature in MultiTest.

🔧 Enabled Bech32 addresses during initialization

In tests, during chain initialization, it’s a common case to fund some tokens to user addresses, that are later used for subsequent bank operations. An example of such test case is shown below. Please note, that a single user address is created using app.api().addr_make function after chain creation but before chain initialization.

use cosmwasm_std::{Coin, Uint128};
use cw_multi_test::App;

const USER: &str = "alice";

let coin = Coin {
denom: "LUNC".to_string(),
amount: Uint128::new(10),
};

// create a chain
let mut app = App::default();

// create single user address used in subsequent operations
let user_addr = app.api().addr_make(USER);

// fund some tokens to user address during chain initialization
app.init_modules(|router, _, storage| {
router
.bank
.init_balance(storage, &user_addr, vec![coin])
.unwrap();
});

// use user address to query it's balances
let balances = app.wrap().query_all_balances(user_addr).unwrap();

assert_eq!("LUNC", balances[0].denom);
assert_eq!(10, balances[0].amount.u128());

The above use case was straightforward, but what if we want to create and initialize the chain in single step? It’s now possible to use the function addr_make which is available in api argument passed to initialization closure. The difference from the previous example is that now we call addr_make function twice: once during chain initialization and once for querying the balances, but the resulting user address is the same.

use cosmwasm_std::{Coin, Uint128};
use cw_multi_test::App;

const USER: &str = "alice";

let coin = Coin {
denom: "LUNC".to_string(),
amount: Uint128::new(10),
};

// create and initialize the chain in one operation
let app = App::new(|router, api, storage| {
router
.bank
.init_balance(storage, &api.addr_make(USER), vec![coin])
.unwrap();
});

// query user's balances
let balances = app
.wrap()
.query_all_balances(app.api().addr_make(USER))
.unwrap();

assert_eq!("LUNC", balances[0].denom);
assert_eq!(10, balances[0].amount.u128());

Another more sophisticated example is shown below. The chain is created and initialized in a single step using AppBuilder, setting up mock Api for generating Bech32m addresses. Like in the previous example, inside the initialization closure, function add_make is used for user address generation. The resulting address will be in Bech32m format with juno prefix.

use cosmwasm_std::{Coin, Uint128};
use cw_multi_test::AppBuilder;
use cw_multi_test::MockApiBech32m;

const USER: &str = "alice";

let coin = Coin {
denom: "LUNC".to_string(),
amount: Uint128::new(10),
};

// create and initialize the chain in one operation
// use Bech32m format for user addresses
// use "juno" prefix for the chain
let app = AppBuilder::new()
.with_api(MockApiBech32m::new("juno"))
.build(|router, api, storage| {
router
.bank
.init_balance(storage, &api.addr_make(USER), vec![coin])
.unwrap();
});

let user_addr = app.api().addr_make(USER);

// just to see how the user address looks like
assert_eq!(
"juno190vqdjtlpcq27xslcveglfmr4ynfwg7gmw86cnun4acakxrdd6gqg3ddu4",
user_addr.as_str()
);

// query user's balances
let balances = app.wrap().query_all_balances(user_addr).unwrap();

assert_eq!("LUNC", balances[0].denom);
assert_eq!(10, balances[0].amount.u128());

The last example shows the usage of custom_app function with initialization closure that uses addr_make function to generate a user address.

use cosmwasm_std::{Coin, CustomMsg, CustomQuery, Uint128};
use cw_multi_test::{custom_app, BasicApp};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

const USER: &str = "alice";

let coin = Coin {
denom: "LUNC".to_string(),
amount: Uint128::new(10),
};

// custom message used in simulated chain
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename = "snake_case")]
pub enum ExampleMsg {
MsgEx,
}
impl CustomMsg for ExampleMsg {}

// custom query used in simulated chain
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename = "snake_case")]
pub enum ExampleQuery {
QueryEx,
}
impl CustomQuery for ExampleQuery {}

// create and initialize a chain with cutom message and custom query
let app: BasicApp<ExampleMsg, ExampleQuery> = custom_app(|router, api, storage| {
router
.bank
.init_balance(storage, &api.addr_make(USER), vec![coin])
.unwrap();
});

// query user's balances
let balances = app
.wrap()
.query_all_balances(app.api().addr_make(USER))
.unwrap();

assert_eq!("LUNC", balances[0].denom);
assert_eq!(10, balances[0].amount.u128());

As a supplement, the usage of addr_make function in different contexts is shown in the following section.

Summary of address generation options in tests

For convenience, there are multiple options available for generating user addresses in tests. The right choice depends on the specific test case. The most popular usages are depicted in the following examples. Please note, that addresses are the same when prefix and format are the same, no matter which method was used to generate it.

Examples using MockApi from cosmwasm-std::testing module

use cosmwasm_std::testing::MockApi;

let addr = MockApi::default().addr_make("owner");
assert_eq!(
"cosmwasm1fsgzj6t7udv8zhf6zj32mkqhcjcpv52yph5qsdcl0qt94jgdckqs2g053y",
addr.as_str()
);
use cosmwasm_std::testing::MockApi;

let addr = MockApi::default().with_prefix("juno").addr_make("owner");
assert_eq!(
"juno1fsgzj6t7udv8zhf6zj32mkqhcjcpv52yph5qsdcl0qt94jgdckqsmg2ndy",
addr.as_str()
);

Examples using MockApiBech32 from cw_multi_test

use cw_multi_test::MockApiBech32;

let addr = MockApiBech32::new("cosmwasm").addr_make("owner");
assert_eq!(
"cosmwasm1fsgzj6t7udv8zhf6zj32mkqhcjcpv52yph5qsdcl0qt94jgdckqs2g053y",
addr.as_str()
);
use cw_multi_test::MockApiBech32;

let addr = MockApiBech32::new("juno").addr_make("owner");
assert_eq!(
"juno1fsgzj6t7udv8zhf6zj32mkqhcjcpv52yph5qsdcl0qt94jgdckqsmg2ndy",
addr.as_str()
);

Examples using MockApiBech32m from cw_multi_test

use cw_multi_test::MockApiBech32m;

let addr = MockApiBech32m::new("cosmwasm").addr_make("owner");
assert_eq!(
"cosmwasm1fsgzj6t7udv8zhf6zj32mkqhcjcpv52yph5qsdcl0qt94jgdckqsl5lc5x",
addr.as_str()
);
use cw_multi_test::MockApiBech32m;

let addr = MockApiBech32m::new("juno").addr_make("owner");
assert_eq!(
"juno1fsgzj6t7udv8zhf6zj32mkqhcjcpv52yph5qsdcl0qt94jgdckqsw56lgx",
addr.as_str()
);

Examples using IntoAddr from cw_multi_test

use cw_multi_test::IntoAddr;

let addr = "owner".into_addr();
assert_eq!(
"cosmwasm1fsgzj6t7udv8zhf6zj32mkqhcjcpv52yph5qsdcl0qt94jgdckqs2g053y",
addr.as_str()
);
use cw_multi_test::IntoAddr;

let addr = "owner".into_addr_with_prefix("juno");
assert_eq!(
"juno1fsgzj6t7udv8zhf6zj32mkqhcjcpv52yph5qsdcl0qt94jgdckqsmg2ndy",
addr.as_str()
);

Examples using IntoBech32 from cw_multi_test

use cw_multi_test::IntoBech32;

let addr = "owner".into_bech32();
assert_eq!(
"cosmwasm1fsgzj6t7udv8zhf6zj32mkqhcjcpv52yph5qsdcl0qt94jgdckqs2g053y",
addr.as_str()
);
use cw_multi_test::IntoBech32;

let addr = "owner".into_bech32_with_prefix("juno");
assert_eq!(
"juno1fsgzj6t7udv8zhf6zj32mkqhcjcpv52yph5qsdcl0qt94jgdckqsmg2ndy",
addr.as_str()
);

Examples using IntoBech32m from cw_multi_test

use cw_multi_test::IntoBech32m;

let addr = "owner".into_bech32m();
assert_eq!(
"cosmwasm1fsgzj6t7udv8zhf6zj32mkqhcjcpv52yph5qsdcl0qt94jgdckqsl5lc5x",
addr.as_str()
);
use cw_multi_test::IntoBech32m;

let addr = "owner".into_bech32m_with_prefix("juno");
assert_eq!(
"juno1fsgzj6t7udv8zhf6zj32mkqhcjcpv52yph5qsdcl0qt94jgdckqsw56lgx",
addr.as_str()
);

Examples using App from cw_multi_test

use cw_multi_test::App;

let app = App::default();

let addr = app.api().addr_make("owner");
assert_eq!(
"cosmwasm1fsgzj6t7udv8zhf6zj32mkqhcjcpv52yph5qsdcl0qt94jgdckqs2g053y",
addr.as_str()
);
use cw_multi_test::{no_init, AppBuilder};

let app = AppBuilder::default()
.with_api(MockApiBech32::new("juno"))
.build(no_init);

let addr = app.api().addr_make("owner");
assert_eq!(
"juno1fsgzj6t7udv8zhf6zj32mkqhcjcpv52yph5qsdcl0qt94jgdckqsmg2ndy",
addr.as_str()
);

🪚 Removed validation of validator addresses

Before the version 2.1.0 of MultiTest, the default implementation of the staking module was checking validators’ addresses against Bech32 format with the same prefix as defined for the chain. While developers were using addr_make function to create validator address used later in tests, everything worked fine. In real-life blockchains, the validator (operator) address has a different prefix when converted to a human-readable format compared to the chain’s prefix. This difference was the reason for mysterious errors indicating an invalid Bech32 format. Since version 2.1.0 of MultiTest, this issue is fixed.
A working example is given in the last code snippet in this article, just compare the following code fragments:

// Validator address, note this has a different prefix that the chain.
let validator_addr = "validator".into_bech32_with_prefix("testvaloper");
// Delegator address has chain's prefix.
let delegator_addr = app.api().addr_make(DELEGATOR);

⚙️ Enabled empty string attributes in tests

In wasmd, since version 0.45.0, it has been allowed to return empty string values in Response attributes. This was not the case in MultiTest until version 2.1.0. Now, in tests, contract developers may use empty string values in attributes, either in Response or in Event. Below is an example of a contract returning empty string attributes.

use cosmwasm_std::{Binary, Deps, DepsMut, Empty, Env,
Event, MessageInfo, Response, StdError};
use cw_multi_test::{Contract, ContractWrapper};

fn instantiate(
_deps: DepsMut,
_env: Env,
_info: MessageInfo,
_msg: Empty,
) -> Result<Response, StdError> {
Ok(Response::default())
}

fn execute(
_deps: DepsMut,
_env: Env,
_info: MessageInfo,
_msg: Empty,
) -> Result<Response, StdError> {
Ok(Response::<Empty>::new()
.add_attribute("city", " ") // <-- empty string
.add_attribute("street", "") // <-- empty string
.add_event(
Event::new("location")
.add_attribute("longitude", " ") // <-- empty string
.add_attribute("latitude", ""), // <-- empty string
))
}

fn query(_deps: Deps, _env: Env, _msg: Empty) -> Result<Binary, StdError> {
Ok(Binary::default())
}

fn contract() -> Box<dyn Contract<Empty>> {
Box::new(ContractWrapper::new_with_empty(execute, instantiate, query))
}

#[test]
fn empty_string_attribute_should_work() {
use cw_multi_test::{App, Executor};

// prepare the blockchain
let mut app = App::default();

// prepare sender address
let sender = app.api().addr_make("sender");

// store the contract's code
let code_id = app.store_code(contract());

// instantiate the contract
let contract_addr = app
.instantiate_contract(
code_id,
sender.clone(),
&Empty{},
&[],
"attributed",
None
).unwrap();

// execute message on the contract, this returns response
// with attributes having empty string values
assert!(app
.execute_contract(sender, contract_addr, &Empty{}, &[])
.is_ok());
}

🗜️ Improved the unbonding

When users stake or bond their tokens to participate in network activities like securing the blockchain, earning rewards, or voting in governance, they often have to wait for a specified period before they can withdraw their staked assets. This period is called the unbonding time. In MultiTest, the check to see if the unbonding time has elapsed is done against the current block time. There is a dedicated function named update_block in the App to advance the block’s height and time. An example how to advance the block is shown below.

use cw_multi_test::App;

let mut app = App::default();

// let's check the default block height and time

let default_height = app.block_info().height;
let default_time = app.block_info().time.nanos();

assert_eq!(12345, default_height);

// timestamp of value 1571797419879305533 represents
// Wed Oct 23 2019 02:23:39 GMT+0000
assert_eq!(1571797419879305533, default_time);

// let's advance the block height with 1,
// and block time with 1 minute
app.update_block(|block| {
block.height += 1;
block.time = block.time.plus_minutes(1);
});

// let's retrieve the new block's height and time

let new_height = app.block_info().height;
let new_time = app.block_info().time.nanos();

// the block height should be advances with 1
assert_eq!(new_height, default_height + 1);

// the block time should be advances with 1 minute
// 1 minute = 60 s = 60_000_000_000 ns
assert_eq!(new_time, default_time + 60_000_000_000);

// the new height is:
assert_eq!(12346, new_height);

// the new timestamp of value 1571797479879305533 represents
// Wed Oct 23 2019 02:24:39 GMT+0000
// so 1 minute later than the default value
assert_eq!(1571797479879305533, new_time);

Please note that in MultiTest, it is possible to advance only one attribute of the block: either height or time. But this is NOT a normal situation in a real-life blockchain, so ALWAYS keep in mind to advance both the block height and block time in tests.

In previous versions of MultiTest there was a very specific situation when the unbonding time was not properly checked, even after advancing both the height and the time of the block, and this issue was fixed in version 2.1.0 of MultiTest.

The example test case with bonding and unbonding process is shown in the following code snippet. To compile this code, please make sure the staking feature is added to cw-multi-test dependency.

use cosmwasm_std::testing::mock_env;
use cosmwasm_std::{coin, Decimal, StakingMsg, Validator};
use cw_multi_test::{AppBuilder, Executor, IntoBech32, StakingInfo};

/// Denominator of the staking token.
const BONDED_DENOM: &str = "stake";

// Time between unbonding and receiving tokens back (in seconds).
const UNBONDING_TIME: u64 = 60;

// Amount of tokens to be (re)delegated
const DELEGATION_AMOUNT: u128 = 100;

// Initial amount of tokens for delegator.
const INITIAL_AMOUNT: u128 = 1000;

// Amount of tokens after delegation.
const FEWER_AMOUNT: u128 = INITIAL_AMOUNT - DELEGATION_AMOUNT;

// Name of the user thet delegates staking.
const DELEGATOR: &str = "delegator";

// Validator address, note this has a different prefix that the chain.
let validator_addr = "validator".into_bech32_with_prefix("testvaloper");

let valoper = Validator::new(
validator_addr.to_string(),
Decimal::percent(10),
Decimal::percent(90),
Decimal::percent(1),
);

// prepare the blockchain configuration
let block = mock_env().block;
let mut app = AppBuilder::default().build(|router, api, storage| {
// set initial balance for the delegator
router
.bank
.init_balance(
storage,
&api.addr_make(DELEGATOR),
vec![coin(INITIAL_AMOUNT, BONDED_DENOM)],
)
.unwrap();
// setup staking parameters
router
.staking
.setup(
storage,
StakingInfo {
bonded_denom: BONDED_DENOM.to_string(),
unbonding_time: UNBONDING_TIME,
apr: Decimal::percent(10),
},
)
.unwrap();
// add a validator
router
.staking
.add_validator(api, storage, &block, valoper)
.unwrap();
});

// Delegator address has chain's prefix.
let delegator_addr = app.api().addr_make(DELEGATOR);

// delegate tokens to validator
app.execute(
delegator_addr.clone(),
StakingMsg::Delegate {
validator: validator_addr.to_string(),
amount: coin(DELEGATION_AMOUNT, BONDED_DENOM),
}
.into(),
)
.unwrap();

// delegation works immediately, so delegator should have now fewer tokens
let delegator_balance = app
.wrap()
.query_balance(delegator_addr.clone(), BONDED_DENOM)
.unwrap();
assert_eq!(FEWER_AMOUNT, delegator_balance.amount.u128());

// validator should have now DELEGATION_AMOUNT of tokens assigned
let delegation = app
.wrap()
.query_delegation(delegator_addr.clone(), validator_addr.clone())
.unwrap()
.unwrap();
assert_eq!(DELEGATION_AMOUNT, delegation.amount.amount.u128());

// now, undelegate all bonded tokens
app.execute(
delegator_addr.clone(),
StakingMsg::Undelegate {
validator: validator_addr.to_string(),
amount: coin(DELEGATION_AMOUNT, BONDED_DENOM),
}
.into(),
)
.unwrap();

// unbonding works with timeout, so tokens will be given back
// after unbonding time; while we do not change the block height
// and time, delegator should still have fewer tokens
let delegator_balance = app
.wrap()
.query_balance(delegator_addr.clone(), BONDED_DENOM)
.unwrap();
assert_eq!(FEWER_AMOUNT, delegator_balance.amount.u128());

// now we update the block height and time that is shorter
// than the unbonding time
app.update_block(|block| {
block.height += 1;
block.time = block.time.plus_seconds(UNBONDING_TIME - 1);
});

// delegator should still have fewer tokens
let delegator_balance = app
.wrap()
.query_balance(delegator_addr.clone(), BONDED_DENOM)
.unwrap();
assert_eq!(FEWER_AMOUNT, delegator_balance.amount.u128());

// now we update the block height and time again,
// so unbonding time is reached
app.update_block(|block| {
block.height += 1;
block.time = block.time.plus_seconds(1);
});

// delegator should now have the initial amount of tokens back
let delegator_balance = app
.wrap()
.query_balance(delegator_addr.clone(), BONDED_DENOM)
.unwrap();
assert_eq!(INITIAL_AMOUNT, delegator_balance.amount.u128());

// there should be no more delegations
let delegation = app
.wrap()
.query_delegation(delegator_addr, validator_addr)
.unwrap();
assert_eq!(None, delegation);

--

--