- Docs
- Smart Contracts
- Submessages
Submessages
In CosmWasm, messages are used to interact with both SDK modules and CosmWasm smart contracts. Typically, these messages are executed in a "set-and-forget" manner, meaning you won't receive any direct response indicating whether the call was successful or not. However, there are scenarios where it is crucial to know the outcome of a call:
- Instantiating a new contract and obtaining the contract address
- Executing an action and verifying that it succeeded (e.g., ensuring that a token transfer to your contract was successful)
- Handling errors from cross-contract calls instead of rolling back the entire transaction
To achieve this, you can use submessages, which allow you to receive the result of the message sent from your smart contract. For more detailed information on submessage semantics and execution order, you can refer to the CosmWasm Semantics documentation.
Creating a submessage
A submessage wraps a CosmosMsg
in a SubMsg
struct, which includes additional details such as a unique identifier, gas limit, and reply strategy.
pub struct SubMsg<T> { pub id: u64, // Unique ID to handle the reply pub msg: CosmosMsg<T>, // The message to be sent pub gas_limit: Option<u64>, // Optional gas limit for the submessage pub reply_on: ReplyOn, // Defines when a reply should be triggered}
You can find the source code for the SubMsg struct here.
Example: instantiating a CW20 token using a submessage
Below is an example of how to create a new CW20 token using a submessage and handle the reply to get the contract address.
use cosmwasm_std::{to_binary, Response, SubMsg, WasmMsg};use cw20::MinterResponse;const INSTANTIATE_REPLY_ID: u64 = 1u64;// Create a message to instantiate a new CW20 tokenlet instantiate_msg = WasmMsg::Instantiate { admin: Some(env.contract.address.to_string()), code_id: msg.cw20_code_id, msg: to_binary(&cw20_base::msg::InstantiateMsg { name: "New Token".to_string(), symbol: "NTKN".to_string(), decimals: 6, initial_balances: vec![], mint: Some(MinterResponse { minter: env.contract.address.to_string(), cap: None, }), })?, funds: vec![], label: "New CW20 Token".to_string(),};// Create a submessage that wraps the message abovelet submessage = SubMsg::reply_on_success(instantiate_msg, INSTANTIATE_REPLY_ID);// Create a response with the submessagelet response = Response::new().add_submessage(submessage);
- Imports:
use cosmwasm_std::{to_binary, Response, SubMsg, WasmMsg};use cw20::MinterResponse;
These lines import necessary types from thecosmwasm_std
andcw20
crates. - Constant Definition:
const INSTANTIATE_REPLY_ID: u64 = 1u64;
This defines a constant ID that will be used to identify the reply from this specific instantiation. - Creating the Instantiate Message:
let instantiate_msg = WasmMsg::Instantiate { ... };
This creates aWasmMsg::Instantiate
struct, which is used to instantiate a new contract. Let's break down its fields:admin: Some(env.contract.address.to_string())
: Sets the admin of the new contract to the current contract's address.code_id: msg.cw20_code_id
: Uses the CW20 code ID provided in the incoming message.msg: to_binary(&cw20_base::msg::InstantiateMsg { ... })?
: This is the instantiation message for the CW20 token:name: "New Token".to_string()
: Sets the token name.symbol: "NTKN".to_string()
: Sets the token symbol.decimals: 6
: Sets the number of decimal places for the token.initial_balances: vec![]
: Initializes with no initial balances.mint: Some(MinterResponse { ... })
: Sets up minting capabilities:minter: env.contract.address.to_string()
: The current contract can mint tokens.cap: None
: No cap on the total supply.
funds: vec![]
: No funds are sent with this instantiation.label: "New CW20 Token".to_string()
: A label for the new contract.
- Creating a Submessage:
let submessage = SubMsg::reply_on_success(instantiate_msg, INSTANTIATE_REPLY_ID);
This wraps the instantiate message in aSubMsg
. Thereply_on_success
function means that a reply will be sent back to this contract only if the instantiation is successful. - Creating the Response:
let response = Response::new().add_submessage(submessage);
This creates a newResponse
and adds the submessage to it. When this response is returned from the contract, it will trigger the instantiation of the new CW20 token contract.
Reply strategies
Submessages offer different reply options that determine when a callback to the contract should occur:
pub enum ReplyOn { /// Always perform a callback after SubMsg is processed Always, /// Only callback if SubMsg returned an error, no callback on success Error, /// Only callback if SubMsg was successful, no callback on error Success, /// Never make a callback (default behavior similar to CosmosMsg) Never,}
In the previous example, we used the SubMsg::reply_on_success shorthand to create a submessage that triggers a reply only on success. You can also explicitly specify the reply strategy when creating a submessage:
let submessage = SubMsg { gas_limit: None, id: INSTANTIATE_REPLY_ID, reply_on: ReplyOn::Success, msg: instantiate_tx.into(),};
Handling a reply
To handle the reply from a submessage, the calling contract must implement a new entry point. This entry point processes the reply based on the unique ID assigned to the submessage.
Example: handling a reply for contract instantiation
#[cfg_attr(not(feature = "library"), entry_point)]pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> StdResult<Response> { match msg.id { INSTANTIATE_REPLY_ID => handle_instantiate_reply(deps, msg), id => Err(StdError::generic_err(format!("Unknown reply id: {}", id))), }}fn handle_instantiate_reply(deps: DepsMut, msg: Reply) -> StdResult<Response> { // Parse and handle the reply data, saving the contract address let res = parse_reply_instantiate_data(msg)?; // Save res.contract_address or perform other actions Ok(Response::new())}
- Entry Point Attribute:
#[cfg_attr(not(feature = "library"), entry_point)]
This attribute marks thereply
function as an entry point for the CosmWasm contract, but only when the "library" feature is not enabled. This allows the same code to be used both as a standalone contract and as a library. - Reply Function Signature:
pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> StdResult<Response>
deps: DepsMut
: Mutable dependencies, allowing the function to modify contract state._env: Env
: Environment variables (unused in this example, hence the underscore).msg: Reply
: The reply message received from a submessage.- Returns a
StdResult<Response>
, which is either a successfulResponse
or an error.
- Match Statement:
match msg.id { INSTANTIATE_REPLY_ID => handle_instantiate_reply(deps, msg), id => Err(StdError::generic_err(format!("Unknown reply id: {}", id))),}
This matches on theid
of the reply message:- If it matches
INSTANTIATE_REPLY_ID
, it callshandle_instantiate_reply
. - For any other id, it returns an error with a message about an unknown reply id.
- If it matches
- Handle Instantiate Reply Function:
fn handle_instantiate_reply(deps: DepsMut, msg: Reply) -> StdResult<Response>
This function specifically handles replies from contract instantiation. - Parsing Reply Data:
let res = parse_reply_instantiate_data(msg)?;
This line parses the reply data. The?
operator will return early if an error occurs during parsing. - Handling the Result:
// Save res.contract_address or perform other actionsOk(Response::new())
This is where you would typically save the new contract's address or perform other actions based on the instantiation result. The function returns an emptyResponse
object, which you might want to modify to include relevant data or events.
Example: handling a reply from a token transfer
use cosmwasm_std::{DepsMut, Event, Reply, Response, StdError, StdResult};const CW20_TRANSFER_REPLY_ID: u64 = 1u64;#[cfg_attr(not(feature = "library"), entry_point)]pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> StdResult<Response> { match msg.id { CW20_TRANSFER_REPLY_ID => handle_transfer_reply(deps, msg), id => Err(StdError::generic_err(format!("Unknown reply id: {}", id))), }}fn handle_transfer_reply(deps: DepsMut, msg: Reply) -> StdResult<Response> { // Process the reply and search for the transfer event msg.result.into_result().map_err(|err| StdError::generic_err(format!("Transfer failed: {}", err)))?; // Find the transfer event in the reply let transfer_event = find_transfer_event(&msg.events) .ok_or_else(|| StdError::generic_err("Unable to find transfer event"))?; // Handle the transfer event and perform necessary actions // For example, you might want to extract and use some attributes from the event let amount = transfer_event.attributes .iter() .find(|attr| attr.key == "amount") .map(|attr| attr.value.clone()) .ok_or_else(|| StdError::generic_err("Unable to find transfer amount"))?; // Perform actions with the transfer information // For example, update some state in the contract // SOME_STATE.save(deps.storage, &amount)?; Ok(Response::new() .add_attribute("action", "handle_transfer_reply") .add_attribute("amount", amount))}fn find_transfer_event(events: &[Event]) -> Option<&Event> { events.iter().find(|e| { e.attributes .iter() .any(|attr| attr.key == "action" && attr.value == "transfer") })}
- Imports and Constant:
use cosmwasm_std::{DepsMut, Event, Reply, Response, StdError, StdResult};const CW20_TRANSFER_REPLY_ID: u64 = 1u64;
This imports necessary types fromcosmwasm_std
and defines a constant ID for the CW20 transfer reply. - Reply Entry Point:
#[cfg_attr(not(feature = "library"), entry_point)]pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> StdResult<Response> { match msg.id { CW20_TRANSFER_REPLY_ID => handle_transfer_reply(deps, msg), id => Err(StdError::generic_err(format!("Unknown reply id: {}", id))), }}
This function is the entry point for handling replies. It matches on the reply ID and callshandle_transfer_reply
for CW20 transfers, or returns an error for unknown IDs. - Handle Transfer Reply:
fn handle_transfer_reply(deps: DepsMut, msg: Reply) -> StdResult<Response> { msg.result.into_result().map_err(|err| StdError::generic_err(format!("Transfer failed: {}", err)))?; let transfer_event = find_transfer_event(&msg.events) .ok_or_else(|| StdError::generic_err("Unable to find transfer event"))?; let amount = transfer_event.attributes .iter() .find(|attr| attr.key == "amount") .map(|attr| attr.value.clone()) .ok_or_else(|| StdError::generic_err("Unable to find transfer amount"))?; Ok(Response::new() .add_attribute("action", "handle_transfer_reply") .add_attribute("amount", amount))}
This function handles the CW20 transfer reply:- It first checks if the transfer was successful.
- It then finds the transfer event using the
find_transfer_event
function. - It extracts the transfer amount from the event's attributes.
- Finally, it returns a
Response
with attributes indicating the action and amount.
- Find Transfer Event:
fn find_transfer_event(events: &[Event]) -> Option<&Event> { events.iter().find(|e| { e.attributes .iter() .any(|attr| attr.key == "action" && attr.value == "transfer") })}
This helper function searches through the events to find the one with an attribute where the key is "action" and the value is "transfer".
Propagation of context between contracts
CosmWasm's design prevents reentrancy attacks by not allowing context to be stored in contract memory. This means that when a contract calls another contract, it can't rely on in-memory state to track the context of that call.
Methods for state propagation
a. Events:
- All events returned by a submessage can be read from the Reply message.
- This allows you to propagate context information through events.
Example:
// In the calling contractlet msg = WasmMsg::Execute { contract_addr: other_contract.to_string(), msg: to_binary(&SomeMsg { /* ... */ })?, funds: vec![],};let submsg = SubMsg::reply_on_success(msg, REPLY_ID);// In the reply handlerfn reply(deps: DepsMut, _env: Env, msg: Reply) -> StdResult<Response> { let event = msg.events.iter().find(|e| e.type == "wasm").unwrap(); let context = event.attributes.iter().find(|a| a.key == "context").unwrap().value; // Use the context...}
b. Temporary State Storage:
- Use
cw_storage_plus::Item
to store temporary state before sending a submessage. - Load this state in the reply handler to manage context across contract calls.
Example:
use cw_storage_plus::Item;const TEMP_CONTEXT: Item<String> = Item::new("temp_context");// Before sending the submessageTEMP_CONTEXT.save(deps.storage, &context_to_save)?;let submsg = SubMsg::reply_on_success(msg, REPLY_ID);// In the reply handlerfn reply(deps: DepsMut, _env: Env, msg: Reply) -> StdResult<Response> { let context = TEMP_CONTEXT.load(deps.storage)?; TEMP_CONTEXT.remove(deps.storage); // Use the context...}
Benefits of this approach
- Prevents reentrancy attacks by not relying on mutable in-memory state.
- Provides a clear and auditable trail of contract interactions.
- Allows for complex multi-contract interactions while maintaining security.
Considerations
- When using events, be mindful of gas costs as emitting many events can be expensive.
- When using temporary storage, remember to clean up (remove) the temporary state after use to avoid bloating the contract's storage.
- Choose between events and temporary storage based on the complexity of your context and the nature of your inter-contract communication.
Best practices
- Use descriptive event types and attribute keys to make your code more readable and debuggable.
- When using temporary storage, use unique keys to avoid conflicts with other parts of your contract.
- Always validate and sanitize any context information received from events or temporary storage before using it.