Skip to main content

Contract semantics

This document aims to clarify the semantics of how a CosmWasm contract interacts with its environment. There are two main types of actions: mutating actions, which receive DepsMut and can modify the state of the blockchain, and query actions, which run on a single node with read-only access to the data.

Execution

The following section will cover how the execute call operates, but it is important to note that the same principles apply to other mutating actions, such as instantiate, migrate, sudo, and so on.

SDK context

Before examining CosmWasm, we should first explore the semantics enforced by the blockchain framework with which it integrates – the Cosmos SDK. This framework is based on the Tendermint BFT Consensus Engine. To better understand this, let's first examine how transactions are processed before they arrive in CosmWasm and after they depart.

First, the Tendermint engine seeks 2/3+ consensus on a list of transactions to be included in the next block. This is done without executing the transactions. They are simply subjected to a minimal pre-filter by the Cosmos SDK module to ensure that they are validly formatted transactions with sufficient gas fees and are signed by an account with enough funds to pay the fees. This means that many transactions that result in an error may be included in a block.

Once a block is committed (typically every 5 seconds or so), the transactions are then fed to the Cosmos SDK sequentially to be executed. Each transaction either returns a result or returns an error, along with event logs, which are recorded in the TxResults section of the next block. The AppHash (or Merkle proof or blockchain state) that is generated after executing the block is also included in the next block.

The Cosmos SDK's BaseApp handles each transaction in an isolated context. First, it verifies all signatures and deducts the gas fees. It sets the "Gas Meter" to limit the execution to the amount of gas paid for by the fees. Finally, it creates an isolated context to run the transaction. This allows the code to read the current state of the chain after the last transaction is finished, but it only writes to a cache that may be committed or rolled back in the event of an error.

A transaction may consist of multiple messages, and each one is executed in turn under the same context and the same gas limit. If all messages succeed, the context will be committed to the underlying blockchain state, and the results of all messages will be stored in the TxResult. If one message fails, all subsequent messages are skipped, and all state changes are reverted. This is crucial for atomicity.

For example, if Alice and Bob both sign a transaction with two messages, such as Alice pays Bob 1,000 ATOM and Bob pays Alice 50 ETH, and Bob doesn't have the funds in his account, Alice's payment would will also be reverted. This is similar to how a typical database transaction works.

The x/wasm is a custom Cosmos SDK module that processes specific messages and uses them to upload, instantiate, and execute smart contracts. In particular, it accepts a properly signed MsgExecuteContract and routes it to Keeper.Execute, which loads the appropriate smart contract and calls execute on it.

Note that this method may return either success (with data and events) or an error. In the case of an error, the entire transaction in the block will be reverted. This is the context in which our contract receives the execute call.

Basic execution

When implementing a contract, we provide the following entry point:

pub fn execute(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, ContractError> { }

With DepsMut, this function can read and write to the backing Storage, as well as use the Api to validate addresses, and Query the state of other contracts or native modules. Once it is done, it returns either Ok(Response) or Err(ContractError). Let's examine what happens next:

If it returns Err, this error is converted into a string representation (err.to_string()), and this is then returned to the SDK module. All state changes are reverted, and x/wasm returns this error message, which will generally (see submessage exception below) abort the transaction and return this same error message to the external caller.

If it returns Ok, then the Response object is parsed and processed. Let's look at the parts here:

pub struct Response<T = Empty>
where
T: Clone + fmt::Debug + PartialEq + JsonSchema,
{
/// Optional list of "subcalls" to make. These will be executed in order
/// (and this contract's subcall_response entry point invoked)
/// *before* any of the "fire and forget" messages get executed.
pub submessages: Vec<SubMsg<T>>,
/// After any submessages are processed, these are all dispatched in the host blockchain.
/// If they all succeed, then the transaction is committed. If any fail, then the transaction
/// and any local contract state changes are reverted.
pub messages: Vec<CosmosMsg<T>>,
/// The attributes that will be emitted as part of a "wasm" event
pub attributes: Vec<Attribute>,
pub data: Option<Binary>,
}

In the Cosmos SDK, a transaction returns a number of events to the user, along with an optional "data result." This result is hashed into the next block's hash to make it provable and can provide essential state information. However, client apps generally rely more on events. The result is also commonly used for passing information between contracts or modules in the SDK. Note that the ResultHash includes only the Code (non-zero indicates an error) and Result (data) from the transaction. Although events and logs are accessible through queries, no light-client proofs are available for them.

If the contract sets data, it will be returned in the Result field. attributes is a list of {key, value} pairs, which will be appended to a default event.

The final result appears like this to the client:

{
"type": "wasm",
"attributes": [
{ "key": "contract_addr", "value": "cosmos1234567890qwerty" },
{ "key": "custom-key-1", "value": "custom-value-1" },
{ "key": "custom-key-2", "value": "custom-value-2" }
]
}

Dispatching messages

Now, let's move on to the messages field. Some contracts only require talking to themselves, such as a CW20 contract that simply adjusts its balances on transfers. However, many contracts want to move tokens (native or CW20) or call into other contracts for more complex actions. This is where messages come in. We return a CosmosMsg, which is a serializable representation of any external call a contract can make.

For example, with the stargate feature flag enabled, it looks like this:

pub enum CosmosMsg<T = Empty>
where
T: Clone + fmt::Debug + PartialEq + JsonSchema,
{
Bank(BankMsg),
/// This can be defined by each blockchain as a custom extension
Custom(T),
Staking(StakingMsg),
Distribution(DistributionMsg),
Stargate {
type_url: String,
value: Binary,
},
Ibc(IbcMsg),
Wasm(WasmMsg),
}

If a contract returns two messages (M1 and M2), both will be parsed and executed in x/wasm with the permissions of the contract (meaning info.sender will be the contract, not the original caller). If they return successfully, they will emit a new event with the custom attributes, and the data field will be ignored. Any messages they return will also be processed. If they return an error, the parent call will return an error, thus rolling back the state of the whole transaction.

Note that the messages are executed depth-first. This means that if contract A returns M1 (WasmMsg::Execute) and M2 (BankMsg::Send), and if contract B (from the WasmMsg::Execute) returns N1 and N2 (for example, StakingMsg and DistributionMsg), the messages will be executed in the following order: M1, N1, N2, M2.

This may be hard to understand at first, and you may wonder why you cannot just call another contract. However, CosmWasm does this to prevent one of the most widespread (and hardest to detect) security holes in Ethereum contracts: Reentrancy. CosmWasm do this by following the actor model, which doesn't nest function calls, but instead returns messages that will be executed later. This means that all the state that is carried over between one call and the next happens in storage and not in memory. For more information on this design, see the Actor Model.

Submessages

As of CosmWasm 0.14 (April 2021), they added yet another way to dispatch calls from the contract, due to the common request for the ability to obtain the result from one of the messages you dispatched. For example, it is now possible to create a new contract with WasmMsg::Instantiate, and then store the address of the newly created contract in the caller with submessages. It also addresses a similar use case of capturing error results, so if you execute a message from, for example, a cron contract, it can store the error message and mark the message as run, rather than aborting the whole transaction. It also allows for limiting the gas usage of the submessage (this is not intended to be used in most cases, but is needed for, for example, the cron job to protect it from an infinite loop in the submessage, which could burn all gas and abort the transaction).

This makes use of CosmosMsg as mentioned above, but it wraps it inside a SubMsg envelope:

pub struct SubMsg<T = Empty>
where
T: Clone + fmt::Debug + PartialEq + JsonSchema,
{
pub id: u64,
pub msg: CosmosMsg<T>,
pub gas_limit: Option<u64>,
pub reply_on: ReplyOn,
}

pub enum ReplyOn {
/// Always perform a callback after SubMsg is processed
Always,
/// Only callback if SubMsg returned an error, no callback on success case
Error,
/// Only callback if SubMsg was successful, no callback on error case
Success,
}

What are the semantics of a submessage execution? First, we create a sub-transaction context around the state, allowing it to read the latest state written by the caller, to write to another cache. If gas_limit is set, it is sandboxed to how much gas it can use until it aborts with OutOfGasError. This error is caught and returned to the caller like any other error returned from contract execution (unless it burned the entire gas limit of the transaction). What is more interesting is what happens upon completion.

If it returns successfully, the temporary state is committed (into the caller's cache), and the Response is processed as normal (an event is added to the current EventManager, and messages and submessages are executed). Once the Response is fully processed, it may then be intercepted by the calling contract (for ReplyOn::Always and ReplyOn::Success). In the event of an error, the subcall will revert any partial state changes due to this message, but will not revert any state changes in the calling contract. The error may then be intercepted by the calling contract (for ReplyOn::Always and ReplyOn::Error). In this case, the message error doesn't abort the entire transaction.

Handling the reply

In order to utilize submessages, the calling contract must possess an additional entry point:

#[entry_point]
pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result<Response, ContractError> { }

pub struct Reply {
pub id: u64,
/// ContractResult is just a nicely serializable version of `Result<SubcallResponse, String>`
pub result: ContractResult<SubcallResponse>,
}

pub struct SubcallResponse {
pub events: Vec<Event>,
pub data: Option<Binary>,
}

After the submessage is finished, the caller will have a chance to handle the result. It will receive the original id of the subcall, which can be used to switch on how to process the result, as well as the Result of the execution, including both success and error cases. Note that it includes all events returned by the submessage, which applies to native SDK modules like Bank, as well as the data returned from the submessage. This, together with the original call id, provides all the context needed to continue processing. If you need more state, you must save some local context to the store (under the id) before returning the submessage in the original execute function and load it in reply. CosmWasm explicitly prohibits passing information through contract memory, as that is the key vector for reentrancy attacks, which represent a large security surface area in Ethereum.

The reply call may itself return an Err, in which case it is treated as if the caller had errored, and the transaction is aborted. However, if the processing is successful, reply may return a normal Response, which will be processed as usual, with events added to the EventManager and all messages and submessages dispatched as described above.

The one critical difference with reply is that we do not drop data. If reply returns data: Some(value) in the Response object, CosmWasm will overwrite the data field returned by the caller. That is, if execute returns data: Some(b"first thought") and the reply (with all the extra information it has access to) returns data: Some(b"better idea"), then this will be returned to the caller of execute (either the client or another transaction), just as if the original execute had returned data: Some(b"better idea"). If reply returns data: None, it will not modify any previously set data state. If there are multiple submessages all setting this, only the last one is used (they all overwrite any previous data value). As a consequence, you can use data: Some(b"") to clear previously set data. This will be represented as a JSON string instead of null and handled as any other Some value.

Order and rollback

Submessages (and their replies) are all executed before any messages. They also follow the depth-first rules, as with messages. Here is a simple example: Contract A returns submessages S1 and S2, and message M1. Submessage S1 returns message N1. The order will be: S1, N1, reply(S1), S2, reply(S2), M1.

Please keep in mind that submessage execution and reply can happen within the context of another submessage. For example, contract-A--submessage --> contract-B--submessage --> contract-C. Then, contract-B can revert the state for contract-C and itself by returning Err in the submessage reply, but not revert contract-A or the entire transaction. It just ends up returning Err to contract-A's reply function.

Note that errors are not handled with ReplyOn::Success, meaning, in such a case, an error will be treated just like a normal message returning an error. This diagram may help explain. Imagine a contract returned two submessages: (a) with ReplyOn::Success and (b) with ReplyOn::Error:

processing a)processing b)reply calledmay overwrite result from replynote
okoka)a)returns success
errerrnonenonereturns error (abort parent transaction)
erroknonenonereturns error (abort parent transaction)
okerra)b)a)b)if both a) and b) overwrite, only b) will be used

Query semantics

Until now, we have focused on the Response object, which allows us to execute code in other contracts via the actor model. This means that each contract is run sequentially, one after the other, and nested calls are not possible. This is essential to avoid reentrancy, which occurs when a call to another contract changes the state while a transaction is in progress.

However, there are many instances when we need access to information from other contracts during processing, such as determining a contract's bank balance before sending funds. To enable this, CosmWasm has enabled the read-only Querier to allow synchronous calls during execution. By making it read-only (and enforcing that at the VM level), CosmWasm can prevent the possibility of reentrancy, as the query cannot modify any state or execute our contract.

When we "make a query," we serialize a QueryRequest struct that represents all possible calls. Then, we pass it over FFI to the runtime, where it is interpreted in the x/wasm SDK module. This process is extensible with blockchain-specific custom queries, just as CosmosMsg accepts custom results. Also, note the ability to perform raw protobuf "Stargate" queries.

pub enum QueryRequest<C: CustomQuery> {
Bank(BankQuery),
Custom(C),
Staking(StakingQuery),
Stargate {
/// this is the fully qualified service path used for routing,
/// eg. custom/cosmos_sdk.x.bank.v1.Query/QueryBalance
path: String,
/// this is the expected protobuf message type (not any), binary encoded
data: Binary,
},
Ibc(IbcQuery),
Wasm(WasmQuery),
}

While this is flexible and necessary for cross-language representation, it can be a bit cumbersome to generate and use when you just want to find your bank balance. To help with this, we often use QuerierWrapper, which wraps a Querier and exposes many convenience methods that use QueryRequest and Querier.raw_query under the hood.

For a more extensive explanation of the Querier design, see Querying Contract State.