First Sudo Message
Your collection manager smart contract can now impose a payment when minting a new name.
If you skipped the previous section, you can just switch:
- The
my-nameservice
project to itsexecute-return-data
branch. - The
my-collection-manager
project to itsproper-fund-handling
branch.
And take it from there.
What if you want to change the price or the beneficiary? You could add a new ExecuteMsg
variant, and gateway it with some parameters. Perhaps you may want to have a vote on it. All this sounds a lot like a governance proposal as it can be implemented on a Cosmos app-chain. What if you could create a governance proposal whose execution is deterministically executed by your smart contract?
That's one of the purposes of sudo messages. A sudo message is one that comes from the underlying app-chain. This is not a user-generated message coming with a transaction.
The mechanism
When running wasmd
or your own app chain you can launch a governance proposal by using the command:
wasmd tx gov submit-proposal sudo-contract --help
This creates a MsgSudoContract
that very much looks like WasmSudo
that you use in the mocked app tests below.
When the proposal is voted in, your smart contract gets called with the bytes msg
part.
The use-case
You implement a sudo message that lets the app-chain change the payment parameters.
Add the payment params query
As it is going to be useful, add a query to get the current payment parameters. In a previous section, you saw in detail how to do that, so here we add them without much explanations:
- use cosmwasm_schema::cw_serde;
+ use cosmwasm_schema::{cw_serde, QueryResponses};
use cosmwasm_std::{Addr, Coin, Empty, Uint128};
...
pub struct NameServiceExecuteMsgResponse {
pub num_tokens: u64,
}
+
+ #[cw_serde]
+ #[derive(QueryResponses)]
+ pub enum QueryMsg {
+ #[returns(GetPaymentParamsResponse)]
+ GetPaymentParams,
+ }
+
+ #[cw_serde]
+ pub struct GetPaymentParamsResponse {
+ pub payment_params: PaymentParams,
+ }
use crate::{
error::ContractError,
msg::{
- CollectionExecuteMsg, CollectionQueryMsg, ExecuteMsg, InstantiateMsg,
- NameServiceExecuteMsgResponse,
+ CollectionExecuteMsg, CollectionQueryMsg, ExecuteMsg, GetPaymentParamsResponse,
+ InstantiateMsg, NameServiceExecuteMsgResponse, QueryMsg,
},
state::PAYMENT_PARAMS,
};
...
use cosmwasm_std::{
- from_json, to_json_binary, BankMsg, Coin, CosmosMsg, DepsMut, Empty, Env, Event, MessageInfo,
- QueryRequest, Reply, ReplyOn, Response, StdError, SubMsg, Uint128, WasmMsg, WasmQuery,
+ from_json, to_json_binary, BankMsg, Coin, CosmosMsg, Deps, DepsMut, Empty, Env, Event,
+ MessageInfo, QueryRequest, QueryResponse, Reply, ReplyOn, Response, StdError, SubMsg, Uint128,
+ WasmMsg, WasmQuery,
};
...
fn reply_pass_through(...) -> ContractResult {
...
}
+ #[cfg_attr(not(feature = "library"), entry_point)]
+ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result<QueryResponse, ContractError> {
+ match msg {
+ QueryMsg::GetPaymentParams {} => Ok(to_json_binary(&GetPaymentParamsResponse {
+ payment_params: PAYMENT_PARAMS.load(deps.storage)?,
+ })?),
+ }
+ }
...
Take this opportunity to replace your dummy query lambda, used in the mocked app test, with the real function:
- use cosmwasm_std::{to_json_binary, Addr, Coin, Empty, Event, Uint128};
+ use cosmwasm_std::{Addr, Coin, Empty, Event, Uint128};
...
use cw_my_collection_manager::{
- contract::{execute, instantiate, reply},
+ contract::{execute, instantiate, query, reply},
msg::{ExecuteMsg, InstantiateMsg, PaymentParams},
};
...
fn instantiate_collection_manager(
...
) -> (u64, Addr) {
- let code = Box::new(
- ContractWrapper::new(execute, instantiate, |_, _, _: ()| {
- to_json_binary("mocked_manager_query")
- })
- .with_reply(reply),
- );
+ let code = Box::new(ContractWrapper::new(execute, instantiate, query).with_reply(reply));
let manager_code_id = mock_app.store_code(code);
...
}
...
At this intermediate stage the my-collection-manager
project should have something similar to the payment-params-query
branch, with this as the diff.
The sudo message
You define your own sudo messages separately from the others. They are not a subset of, say, ExecuteMsg
:
...
pub struct GetPaymentParamsResponse {
pub payment_params: PaymentParams,
}
+
+ #[cw_serde]
+ pub enum SudoMsg {
+ UpdatePaymentParams(PaymentParams),
+ }
Sudo handling
To handle sudo messages, you need to add the sudo
entry point. This is the one that the CosmWasm module will invoke when the system instructs it to handle a sudo message. Here too, the sudo
function only matches the variant and then invokes a specialized sudo function:
use crate::{
msg::{
CollectionExecuteMsg, CollectionQueryMsg, ExecuteMsg, GetPaymentParamsResponse,
- InstantiateMsg, NameServiceExecuteMsgResponse, QueryMsg,
+ InstantiateMsg, NameServiceExecuteMsgResponse, PaymentParams, QueryMsg, SudoMsg,
},
}
...
+ #[cfg_attr(not(feature = "library"), entry_point)]
+ pub fn sudo(deps: DepsMut, _env: Env, msg: SudoMsg) -> ContractResult {
+ match msg {
+ SudoMsg::UpdatePaymentParams(payment_params) => {
+ sudo_update_payment_params(deps, payment_params)
+ }
+ }
+ }
+
+ fn sudo_update_payment_params(deps: DepsMut, payment_params: PaymentParams) -> ContractResult {
+ payment_params.validate()?;
+ PAYMENT_PARAMS.save(deps.storage, &payment_params)?;
+ let sudo_event = Event::new("my-collection-manager");
+ let sudo_event = append_payment_params_attributes(sudo_event, payment_params);
+ Ok(Response::default().add_event(sudo_event))
+ }
+ fn append_payment_params_attributes(my_event: Event, payment_params: PaymentParams) -> Event {
+ let my_event = my_event.add_attribute(
+ "update-payment-params-beneficiary",
+ payment_params.beneficiary,
+ );
+ match payment_params.mint_price {
+ None => my_event.add_attribute("update-payment-params-mint-price", "none"),
+ Some(mint_price) => my_event
+ .add_attribute("update-payment-params-mint-price-denom", mint_price.denom)
+ .add_attribute(
+ "update-payment-params-mint-price-amount",
+ mint_price.amount.to_string(),
+ ),
+ }
+ }
#[cfg(test)]
mod tests...
Note that:
- Just as in instantiate, it verifies that the new parameters are valid.
- Just as in execute, it emits an event to inform on the change.
- The new function that appends values to the event can be reused.
Adjust instantiate for good measure
Now that the payment parameters can change, and with a goal of symmetry, you can adjust the instantiate
function to also emit an event. This has the added benefit that the history of the payment parameters value can be reconstructed from the events alone.
..
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(deps: DepsMut, _: Env, _: MessageInfo, msg: InstantiateMsg) -> ContractResult {
msg.payment_params.validate()?;
PAYMENT_PARAMS.save(deps.storage, &msg.payment_params)?;
+ let instantiate_event = Event::new("my-collection-manager");
+ let instantiate_event = append_payment_params_attributes(instantiate_event, msg.payment_params);
- Ok(Response::default())
+ Ok(Response::default().add_event(instantiate_event))
}
...
Unit tests
It is worth adding a unit test that calls the function in isolation and confirms it returns as expected.
...
mod tests {
use crate::{
contract::ReplyCode,
msg::{
CollectionExecuteMsg, CollectionQueryMsg, ExecuteMsg, InstantiateMsg,
- NameServiceExecuteMsgResponse, PaymentParams,
+ NameServiceExecuteMsgResponse, PaymentParams, SudoMsg,
},
+ state::PAYMENT_PARAMS,
};
...
fn test_reply_pass_through() {
...
}
+
+ #[test]
+ fn test_sudo_update_payment_params() {
+ // Arrange
+ let mut mocked_deps_mut = testing::mock_dependencies();
+ let mocked_env = testing::mock_env();
+ let beneficiary = Addr::unchecked("beneficiary");
+ let new_payment_params = PaymentParams {
+ beneficiary: beneficiary.to_owned(),
+ mint_price: Some(Coin {
+ denom: "silver".to_owned(),
+ amount: Uint128::one(),
+ }),
+ };
+ let sudo_msg = SudoMsg::UpdatePaymentParams(new_payment_params.to_owned());
+
+ // Act
+ let contract_result = super::sudo(mocked_deps_mut.as_mut(), mocked_env, sudo_msg);
+
+ // Assert
+ assert!(contract_result.is_ok(), "Failed to sudo");
+ let received_response = contract_result.unwrap();
+ let expected_response = Response::default().add_event(
+ Event::new("my-collection-manager")
+ .add_attribute("update-payment-params-beneficiary", beneficiary)
+ .add_attribute("update-payment-params-mint-price-denom", "silver")
+ .add_attribute("update-payment-params-mint-price-amount", "1"),
+ );
+ assert_eq!(received_response, expected_response);
+ let payment_params = PAYMENT_PARAMS
+ .load(&mocked_deps_mut.storage)
+ .expect("Failed to load payment params");
+ assert_eq!(payment_params, new_payment_params);
+ }
}
Note that:
- It is not strictly necessary to first call the instantiate as the test only writes to storage.
Mocked app tests
You can instruct the mocked app to pass a sudo msg to your compiled smart contract, as long as you compiled it with the sudo
function.
...
use cw721::msg::{Cw721ExecuteMsg, Cw721QueryMsg, OwnerOfResponse};
- use cw_multi_test::{App, AppBuilder, ContractWrapper, Executor};
+ use cw_multi_test::{App, AppBuilder, ContractWrapper, Executor, WasmSudo};
use cw_my_collection_manager::{
- contract::{execute, instantiate, query, reply},
- msg::{ExecuteMsg, InstantiateMsg, PaymentParams},
+ contract::{execute, instantiate, query, reply, sudo},
+ msg::{ExecuteMsg, GetPaymentParamsResponse, InstantiateMsg, PaymentParams, QueryMsg, SudoMsg},
};
...
fn instantiate_collection_manager(
...
) -> (u64, Addr) {
let code = Box::new(
ContractWrapper::new(execute, instantiate, query)
- .with_reply(reply),
+ .with_reply(reply)
+ .with_sudo(sudo),
);
...
}
...
+ #[test]
+ fn test_sudo_update_payment_params() {
+ // Arrange
+ let mut mock_app = App::default();
+ let beneficiary_addr = Addr::unchecked("beneficiary");
+ let (_, addr_manager) = instantiate_collection_manager(
+ &mut mock_app,
+ PaymentParams {
+ beneficiary: beneficiary_addr.to_owned(),
+ mint_price: None,
+ },
+ );
+ let new_payment_params = PaymentParams {
+ beneficiary: beneficiary_addr.to_owned(),
+ mint_price: Some(Coin {
+ denom: "silver".to_owned(),
+ amount: Uint128::from(23u16),
+ }),
+ };
+ let update_sudo_msg = SudoMsg::UpdatePaymentParams(new_payment_params.to_owned());
+ let sudo_msg = cw_multi_test::SudoMsg::Wasm(
+ WasmSudo::new(&addr_manager, &update_sudo_msg).expect("Failed to serialize sudo message"),
+ );
+
+ // Act
+ let result = mock_app.sudo(sudo_msg);
+
+ // Assert
+ assert!(result.is_ok(), "Failed to pass through the message");
+ let result = result.unwrap();
+ let expected_sudo_event = Event::new("wasm-my-collection-manager")
+ .add_attribute("_contract_address", addr_manager.to_owned())
+ .add_attribute("update-payment-params-beneficiary", beneficiary_addr)
+ .add_attribute("update-payment-params-mint-price-denom", "silver")
+ .add_attribute("update-payment-params-mint-price-amount", "23");
+ result.assert_event(&expected_sudo_event);
+ let result = mock_app
+ .wrap()
+ .query_wasm_smart::<GetPaymentParamsResponse>(&addr_manager, &QueryMsg::GetPaymentParams);
+ assert!(result.is_ok(), "Failed to query payment params");
+ assert_eq!(
+ result.unwrap(),
+ GetPaymentParamsResponse {
+ payment_params: new_payment_params
+ }
+ );
+ }
Note that:
- Note that your smart contract's sudo message is first wrapped into the testing frameworks sudo message.
- The mocked app tests only test that the contract handles a sudo message coming from the app. It does not test that, given a properly formed governance proposal, the smart contract is called on sudo. Testing that would be like testing a mocked app feature.
Conclusion
Now your smart contract is able to receive instructions from the underlying app chain to update the payment parameters. By leveraging the Cosmos SDK's governance module, you only have to code the effect of successful proposals.
At this stage:
- The
my-nameservice
project should still have something similar to theexecute-return-data
branch. - The
my-collection-manager
project should have something similar to thesudo-message
branch, with this as the diff from the payment params query and this as the larger diff from the previous section.