CosmWasm

Please make sure you're familiar with the Cosmos execution model before reading this page. Also, the CosmWasm book is the official resource for CosmWasm, and will include information not contained in this page.

The Cosmos blockchain ecosystem is modular, allowing different submodules to be enabled or disabled for an individual chain. CosmWasm is one of the most common add-ons. It is a smart contract language built on WASM and Rust. Some highlights:

  • CosmWasm (and Cosmos overall) is a single-threaded execution model.
  • Contract execution is either success or failure. A failed execution will not write any changes to the chain state. Think of this like a database rollback.
  • Contracts are able to query other Cosmos modules, other contracts, raw storage, and more.
  • Contracts are able to spawn submessages. The contract can determine whether a failing submessage will cause the entire transaction to fail or not.
  • Contracts are (generally) written in Rust, compiled to WASM, and then uploaded to a chain. When you upload, you get a code ID for that upload, and
  • Every contract has its own data storage. This is a key/value store of arbitrary binary data (though in storage we'll discuss common techniques for easing storage interaction). Only a contract can write to its own storage, though other tools and contracts can query the data storage using a raw query.
  • Contracts have entrypoints, which is how you interact with them. The most common entrypoints are:
    • Instantiate: used when instantiating a code ID into a live contract. The result of instantiation will be a fresh contract with its own contract address.
    • Query (aka smart query): perform a read-only query against the contract.
    • Execute: perform an action on the contract. This is open to all accounts, though authentication can be performed within the contract itself.
    • Migrate: each contract can optionally have a chain-level admin that is capable of performing a migration. Migrations can be used to move to a new code ID, and can also optionally run arbitrary code during a migration to--for example--update data in the contract.
    • Reply: used for handling callbacks for running submessages. For example, a contract may emit a submessage to transfer funds from the contract to a user wallet (e.g., when collecting staking rewards). Optionally, you can specify that after running the submessage, the reply entrypoint of your contract should be called with the execution result. This can allow you to do things like handling insufficient funds without just aborting the entire transaction.
  • Interactions with contract entrypoints is (virtually always) done via JSON requests and responses. Rust's serde library is heavily used by CosmWasm for making it easy to generate these message types. It's common to separate out message types to their own Rust crate and generate JSON schema files for use by external tools.

Scaffold

There are lots of different ways of structuring CosmWasm contracts. The only required bit is that the entrypoints need to be public, plus some rules around setting up the library crate with proper wasm config. You can check out a simple starter than includes a test framework at:

https://github.com/snoyberg/cosmwasm-starter

Some comments:

  • You'll need to include crate-type = ["cdylib"] in the Cargo.toml's lib section.
  • Most projects seem to use thiserror for error handling, though anyhow works well too. Even though our general advice is to use anyhow for application error handling, in the case of smart contracts using thiserror can be preferable so that external tools can display custom error messages.
  • Execute and query message types are generally enums, while other entry points use structs.

Cosmos vs contract message types

A complication around messages is that, with CosmWasm, you have two layers of "messages." And, for that matter, two layers of "queries." Let's clarify.

Cosmos chains make extensive use of protobufs and gRPC to define their messages and queries, as well as the services that support those queries. I'll use the generated Rust docs for demonstrating those since I'm most familiar with those.

When you want to upload a new contract to the blockchain, you need to do what's called "store code." This is a message called MsgStoreCode. This message is part of the CosmWasm module of Cosmos, and can be included in a transaction just like other messages (like MsgSend). At this point, there's only one layer of messages: Cosmos chain messages.

Once you store code, you'll get back a code ID. Now you'll want to instantiate the contract. To do this, you'll need to send a MsgInstantiateContract message. This data structure includes the code_id from the store code action. But it also contains a msg: Vec<u8> field, which is the encapsulated contract-recognized JSON message for the instantiate entrypoint. This is where the two layers come into play:

  • The Cosmos chain itself sees the MsgInstantiateContract. It then grabs the msg field from that, and then...
  • The chain will run your smart contract WASM code, providing it the msg field (and other metadata). The contract is responsible for parsing the JSON into the correct data type and then processing the instantiation.

The same logic applies to execution: you send a MsgExecuteContract and include a msg field within it.

Queries are different, since they don't perform any actions and are not included in a transaction. Instead of having messages, we have QuerySmartContractStateRequest. This can be sent to a node without signing a transaction, and includes the JSON message sent to the contract code in the query_data field.

So, in sum:

  • Each smart contract defines its own entry points and the message types they accept.
  • These messages get wrapped up inside Cosmos messages and queries designed for handling smart contracts.

Entrypoint example

Here's an example of a real life entrypoint. This is an execute entry point from a contract which handles "secure transfers": it ensures that funds are transferred to each wallet only one time. It's intended to help with airdrops and avoid accidentally double-funding people, which can happen if you accidentally rerun a normal MsgSend. Instead, by tracking everything in the smart contract, the smart contract can be responsible for rejecting any attempts to airdrop to the same wallet twice.

#![allow(unused)]
fn main() {
#[entry_point]
pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> Result<Response> {
    let admin = ADMIN.load(deps.storage)?;
    if info.sender != admin {
        return Err(Error::NotTheAdmin {
            admin,
            sender: info.sender,
        });
    }

    match msg {
        ExecuteMsg::Transfer { recipients, ident } => {
            check_ident(deps.storage, &ident)?;
            transfer(deps, recipients)
        }
        ExecuteMsg::Retrieve { ident } => {
            check_ident(deps.storage, &ident)?;
            retrieve(deps, admin, env)
        }
    }
}

fn check_ident(store: &dyn Storage, ident: &str) -> Result<()> {
    let actual_ident = IDENT.load(store)?;
    if ident == actual_ident {
        Ok(())
    } else {
        Err(Error::MismatchedIdent {
            provided: ident.to_owned(),
            actual_ident,
        })
    }
}

const RECIPIENTS: Map<Addr, Vec<Coin>> = Map::new("recipients");

fn transfer(deps: DepsMut, recipients: Vec<Recipient>) -> Result<Response> {
    let mut res = Response::new();

    for Recipient { addr, coins } in recipients {
        let addr = deps.api.addr_validate(&addr)?;
        if let Some(coins) = RECIPIENTS.may_load(deps.storage, addr.clone())? {
            return Err(Error::AlreadyTransferedTo { addr, coins });
        }
        RECIPIENTS.save(deps.storage, addr.clone(), &coins)?;
        res = res.add_message(BankMsg::Send {
            to_address: addr.into_string(),
            amount: coins,
        });
    }
    Ok(res)
}
}

Some highlights to pay attention to in the code above:

  • The storage mechanism we use is cw-storage-plus, discussed below.
  • Entrypoints use the #[entry_point] attribute macro to generate some boilerplate code.
  • Note that the execute entrypoint performs admin checking inside of it, following a common pattern of erroring out if the user has no permissions. Remember that if a contract exits with an error, all storage changes it may have performed will be wiped out.
  • The execute endpoint receives some parameters that help with processing the data. A rundown:
    • The msg: ExecuteMsg always contains our locally defined data type for execute messages. The #[entry_point] macro generates code for parsing and validating the raw bytes sent into the chain.
    • info: MessageInfo contains information on the user that submitted the transaction and any funds they sent along with it.
    • env: Env contains information on the status of the blockchain (block height and time) and the contract that is being executed.
    • deps: DepsMut provides three fields:
      • querier: QuerierWrapper is used for querying the chain for things like token balances.
      • api: &dyn Api is primarily used for validating that wallet addresses are valid.
      • storage: &mut dyn Storage provides the ability to read from and write to the contract's storage.
      • Note that, in addition to DepsMut, there's also a Deps data type. This is used in the query entrypoint, and provides a read-only storage: &dyn Storage field instead, which provides a type-safe way to ensure that queries can't write to storage.

Common libraries

  • cosmwasm-std is the core library for CosmWasm, providing helper functions and data types for many common and primitives operations.
  • cw-storage-plus is the de facto standard storage library, providing a nice abstraction over the Storage trait for handling things like singleton, maps, and more. I wouldn't describe this library as stellar, but it's Good Enough, and sticking to it simplifies code and helps us avoid corner cases.
  • Error handling is pretty standard, using one of the following commonly used libraries.
  • cw-multi-test is great for writing unit and integration tests. It will simulate a chain environment without needing to run a full chain. We typically call tests using cw-multi-test "off-chain tests." It's a good idea for important projects to also have on-chain testing, where you deploy the contract to the chain and run a series of actions. Usually you would use something like local Osmosis.

Storage

Generally stick to cw-storage-plus for storage.

Smart vs raw queries

CosmWasm smart contracts are a combination of code and storage. Most of the time we interact with smart contracts, it’s through an entrypoint. The two most common entrypoints are execute and query. execute is performed in a transaction and lands on-chain, requires gas and a wallet to send it from, and can make arbitrary changes to storage. query, or better named right now “smart query,” is a read-only message that runs the contract code on some input JSON message and gives back a JSON response. It’s a “smart” query because (1) it’s a smart contract and (2) it performs a smart piece of logic embedded in the contract.

However, there’s one other way to query the storage of a contract: a raw query. Everything that gets stored in contract storage does so through a simple key/value store, where the keys and values are both arbitrary binary blobs. Internally to the smart contract, if you dig deep enough through the helper libraries, you’ll eventually find code that directly interacts with this key/value store.

However, you can also do a raw query from outside of the contract. This can be useful primarily for two reasons:

  • Get access to some data that’s not exposed through a smart query.
  • Reduce gas costs: a smart query requires running the blockchain’s VM system, which is relatively expensive. Raw queries are significantly cheaper.