Introduction

This book is a guide for creating CosmWasm smart contracts with Sylvia framework. It will lead you step by step, and explain relevant topics from the easiest to the trickier ones.

The idea of the book is not only to tell you about smart contracts API but also to show you how to do it in a clean and maintainable way. We will show you patterns that CosmWasm creators established and encouraged you to use.

This book is a redo of cosmwasm-book. To see advantage of using sylvia you can compare development of smart contracts in both of these titles.

Prerequirements

This book explores CosmWasm smart contracts. It is not a Rust tutorial, and it assumes basic Rust knowledge. As you will probably learn it alongside this book, I strongly recommend grasping the language itself first. You can find great resources to start with Rust on Learn Rust page.

CosmWasm API documentation

This is the guide-like documentation. If you are looking for the API documentation, you may be interested in checking one of the following:

Contributing to the book

This book is maintained on Github and auto deployed from there. Please create an issue or pull request if you find any mistakes, bugs, or ambiguities.

Warning

This book is still under construction so be aware that in places it might feel a little bit disjointed.

Setting up the environment

To work with CosmWasm smart contract, you will need rust installed on your machine. If you don't have one, you can find installation instructions on the Rust website.

I assume you are working with a stable Rust channel in this book.

Additionally, you will need the Wasm rust compiler backend installed to build Wasm binaries. To install it, run:

rustup target add wasm32-unknown-unknown

Optionally if you want to try out your contracts on a testnet, you will need a wasmd binary. We would focus on testing contracts with Rust unit testing utility throughout the book, so it is not required to follow. However, seeing the product working in a real-world environment may be nice.

To install wasmd, first install the golang. Then clone the wasmd and install it:

$ git clone git@github.com:CosmWasm/wasmd.git
$ cd ./wasmd
$ make install

Also, to be able to upload Rust Wasm Contracts into the blockchain, you will need to install docker. To minimize your contract sizes, it will be required to run CosmWasm Rust Optimizer; without that, more complex contracts might exceed a size limit.

Check contract utility

An additional helpful tool for building smart contracts is the cosmwasm-check utility. It allows you to check if the wasm binary is a proper smart contract ready to upload into the blockchain. You can install it using cargo:

$ cargo install cosmwasm-check

If the installation succeeds, you should be able to execute the utility from your command line.

$ cosmwasm-check --version
Contract checking 1.1.6

Verifying the installation

To guarantee you are ready to build your smart contracts, you need to make sure you can build examples. Checkout the sylvia repository and run the testing command in its folder:

$ git clone git@github.com:CosmWasm/sylvia.git
$ cd ./sylvia
sylvia $ cargo test

You should see that everything in the repository gets compiled, and all tests pass.

sylvia framework contains some examples of contracts. To find them go to contracts directory. These contracts are maintained by CosmWasm creators, so contracts in there should follow good practices.

To verify the cosmwasm-check utility, first, you need to build a smart contract. Go to some contract directory, for example, contracts/cw1-whitelist, and call cargo wasm:

cw-plus $ cd contracts/cw1-whitelist
cw-plus/contracts/cw1-whitelist $ cargo wasm

wasm is an alias for wasm = "build --release --target wasm32-unknown-unknown --lib". You should be able to find your output binary in the target/wasm32-unknown-unknown/release/ of the root repo directory - not in the contract directory itself! Now you can check if contract validation passes:

sylvia $ cosmwasm-check target/wasm32-unknown-unknown/release/cw1_whitelist.wasm
Available capabilities: {"cosmwasm_1_1", "iterator", "stargate", "staking"}

target/wasm32-unknown-unknown/release/cw1_whitelist.wasm: pass

All contracts (1) passed checks!

Macro expansion

Sylvia generates a lot of code for us which is not visible in code. To see what code is generated with it go to contracts/cw1-whitelist/src/contract.rs. In VSCode you can click on #[contract], do shift+p and then type: rust analyzer: Expand macro recursively. This will open a window with fully expanded macro which you can browse. This is also possible f.e. in VIM depending on your configuration. You can also use cargo expand tool from CLI for this.

Quick start with wasmd

This section is a quick guide on working with wasmd to test your smart contracts on the actual blockchain. Note that this whole section is entirely optional - if you want to develop contracts and test them with the UT environment, feel free to skip it.

Testnet setup

To interact with a blockchain test net, the first thing to do is pick one. I suggest our generic CosmWasm test net malaga-420. As wasmd is configured via environment variables, we will start with creating a malaga.env file that sets them to proper values:

export CHAIN_ID="malaga-420"
export TESTNET_NAME="malaga-420"
export FEE_DENOM="umlg"
export STAKE_DENOM="uand"
export BECH32_HRP="wasm"
export WASMD_VERSION="v0.27.0"
export CONFIG_DIR=".wasmd"
export BINARY="wasmd"

export GENESIS_URL="https://raw.githubusercontent.com/CosmWasm/testnets/master/malaga-420/config/genesis.json"

export RPC="https://rpc.malaga-420.cosmwasm.com:443"
export FAUCET="https://faucet.malaga-420.cosmwasm.com"

export COSMOVISOR_VERSION="v0.42.10"
export COSMOVISOR_HOME=/root/.wasmd
export COSMOVISOR_NAME=wasmd

export NODE=(--node $RPC)
export TXFLAG=($NODE --chain-id $CHAIN_ID --gas-prices 0.05umlg --gas auto --gas-adjustment 1.3)

If you are a fish user, this malaga.fish file may fit you better:

set -x CHAIN_ID malaga-420
set -x TESTNET_NAME malaga-420
set -x FEE_DENOM umlg
set -x STAKE_DENOM uand
set -x BECH32_HRP wasm
set -x WASMD_VERSION v0.27.0
set -x CONFIG_DIR .wasmd
set -x BINARY wasmd

set -x GENESIS_URL https://raw.githubusercontent.com/CosmWasm/testnets/master/malaga-420/config/genesis.json

set -x RPC https://rpc.malaga-420.cosmwasm.com:443
set -x FAUCET https://faucet.malaga-420.cosmwasm.com

set -x COSMOVISOR_VERSION v0.42.10
set -x COSMOVISOR_HOME /root/.wasmd
set -x COSMOVISOR_NAME wasmd

set -x NODE $RPC
set -x TXFLAG --node $RPC --chain-id $CHAIN_ID --gas-prices 0.05umlg --gas-adjustment 1.3 --gas auto -b block

Now source the file to our environment (for fish use malaga.fish in place of malaga.env):

$ source ./malaga.env

Preparing account

The first thing you need to interact with testnet is a valid account. Start with adding a new key to the wasmd configuration:

$ wasmd keys add wallet
- name: wallet
  type: local
  address: wasm1wukxp2kldxae36rgjz28umqtq792twtxdfe6ux
  pubkey: '{"@type":"/cosmos.crypto.secp256k1.PubKey","key":"A8pamTZH8x8+8UAFjndrvU4x7foJbCvcz78buyQ8q7+k"}'
  mnemonic: ""
...

As a result of this command, you get information about just the prepared account. Two things are relevant here:

  • address is your identity in the blockchain
  • mnemonic (omitted by myself in the example) is 12 words that allow you to recreate an account so you can use it, for example, from a different machine

For testing purposes, storing the mnemonic is probably never necessary, but it is critical information to keep safe in the real world.

Now, when you create an account, you have to initialize it with some tokens - you will need them to pay for any interaction with blockchain - we call this the "gas cost" of an operation. Usually, you would need to buy those tokens somehow, but in testnets, you can typically create as many tokens as you want on your accounts. To do so on malaga network, invoke:

$ curl -X POST --header "Content-Type: application/json" \
  --data '{ "denom": "umlg", "address": "wasm1wukxp2kldxae36rgjz28umqtq792twtxdfe6ux" }' \
  https://faucet.malaga-420.cosmwasm.com/credit

It is a simple HTTP POST request to the https://faucet.malaga-420.cosmwasm.com/credit endpoint. The data of this request is a JSON containing the name of a token to mint and the address which should receive new tokens. Here we are minting umlg tokens, which are tokens used to pay gas fees in the malaga testnet.

You can now verify your account tokens balance by invoking (substituting my address with yours):

$ wasmd query bank balances wasm1wukxp2kldxae36rgjz28umqtq792twtxdfe6ux
balances:
- amount: "100000000"
  denom: umlg
pagination:
  next_key: null
  total: "0"

100M tokens should be plenty for playing around, and if you need more, you can always mint another batch.

Interaction with testnet

Blockchain interaction is performed using the wasmd command-line tool. To start working with the testnet, we need to upload some smart contract code. For now, we would use an example cw4-group from the cw-plus repository. Start with cloning it:

$ git clone git@github.com:CosmWasm/cw-plus.git

Now go to cloned repo and run Rust optimizer on it:

$ docker run --rm -v "$(pwd)":/code \
  --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \
  --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \
  cosmwasm/workspace-optimizer:0.12.6

After a couple of minutes - it can take some for the first time - you should have an artifact directory in your repo, and there should be a cw4-group.wasm file being the contract we want to upload. To do so, run - note that wallet is name of the key you created in the previous chapter:

$ wasmd tx wasm store ./artifacts/cw4_group.wasm --from wallet $TXFLAG -y -b block

...
logs:
- events:
  - attributes:
    - key: action
      value: /cosmwasm.wasm.v1.MsgStoreCode
    - key: module
      value: wasm
    - key: sender
      value: wasm1wukxp2kldxae36rgjz28umqtq792twtxdfe6ux
    type: message
  - attributes:
    - key: code_id
      value: "12"
    type: store_code
...

As a result of execution, you should get a pretty long output with information about what happened. Most of this is an ancient cipher (aka base64) with execution metadata, but what we are looking for is the logs section. There should be an event called store_code, with a single attribute code_id - its value field is the code id of our uploaded contract - 12 in my case.

Now, when we have our code uploaded, we can go forward and instantiate a contract to create its new instance:

$ wasmd tx wasm instantiate 12 \
  '{ "admin": "wasm1wukxp2kldxae36rgjz28umqtq792twtxdfe6ux", "members": [] }' \
  --from wallet --label "Group" --no-admin $TXFLAG -y

...
logs:
- events:
  - attributes:
    - key: _contract_address
      value: wasm18yn206ypuxay79gjqv6msvd9t2y49w4fz8q7fyenx5aggj0ua37q3h7kwz
    - key: code_id
      value: "12"
    type: instantiate
  - attributes:
    - key: action
      value: /cosmwasm.wasm.v1.MsgInstantiateContract
    - key: module
      value: wasm
    - key: sender
      value: wasm1wukxp2kldxae36rgjz28umqtq792twtxdfe6ux
    type: message
...

In this command, the 12 is the code id - the result of uploading the code. After that, a JSON is an instantiation message - I will talk about this later. Just think about it as a message requiring fields to create a new contract. Every contract has its instantiation message format. For cw4-group, there are two fields: admin is an address that would be eligible to execute messages on this contract. It is crucial to set it to your address, as we will want to learn how to execute contracts. members is an array of addresses that are initial members of the group. We leave it empty for now, but you can put any addresses you want there. Here, I put one hint about messages inline into the command line, but I often put messages to be sent to the file and embed them via $(cat msg.json). It is fish syntax, but every shell provides a syntax for this.

Then after the message, you need to add a couple of additional flags. The --from wallet is the same as before - the name of the key you created earlier. --label "Group" is just an arbitrary name for your contract. An important one is a --no-admin flag - keep in mind that it is a different "admin" that we set in the instantiation message. This flag is relevant only for contract migrations, but we won't cover them right now, so leave this flag as it is.

Now, look at the result of the execution. It is very similar to before - much data about the execution process. And again, we need to take a closer look into the logs section of the response. This time we are looking at an event with type instantiate, and the _contract_address attribute - its value is newly created contract address - wasm1wukxp2kldxae36rgjz28umqtq792twtxdfe6ux in an example.

Now let's go forward with querying our contract:

$ wasmd query wasm contract-state smart \
  wasm18yn206ypuxay79gjqv6msvd9t2y49w4fz8q7fyenx5aggj0ua37q3h7kwz \
  '{ "list_members": {} }'

data:
  members: []

Remember to change the address (right after smart) with your contract address. After that, there is another message - this time the query message - which is sent to the contract. This query should return a list of group members. And in fact, it does - response is a single data object with a single field - empty members list. That was easy, now let's try the last thing: the execution:

$ wasmd tx wasm execute \
  wasm18yn206ypuxay79gjqv6msvd9t2y49w4fz8q7fyenx5aggj0ua37q3h7kwz \
  '{ "update_members": { "add": [{ "addr": "wasm1wukxp2kldxae36rgjz28umqtq792twtxdfe6ux", "weight": 1 }], "remove": [] } }' \
  --from wallet $TXFLAG

As you can see, execution is very similar to instantiation. The differences are, that instantiation is called just once, and execution needs a contract address. It is fair to say that instantiation is a particular case for first execution, which returns the contract address. Just like before we can see that we got some log output - you can analyze it to see that something probably happened. But to ensure that there is an effect on blockchain, the best way would be to query it once again:

$ wasmd query wasm contract-state smart \
  wasm18yn206ypuxay79gjqv6msvd9t2y49w4fz8q7fyenx5aggj0ua37q3h7kwz \
  '{ "list_members": {} }'

data:
  members:
  - addr: wasm1wukxp2kldxae36rgjz28umqtq792twtxdfe6ux
    weight: 1

For the time being, this is all you need to know about wasmd basics in order to be able to play with your simple contracts. We would focus on testing them locally, but if you want to check in real life, you have some basics now. We will take a closer look at wasmd later when we would talk about the architecture of the actor model defining communication between smart contracts.

Building the contract

Now it is time to build our contract. We can use a traditional cargo build pipeline for local testing purposes: cargo build for compiling it and cargo test for running all tests (which we don't have yet, but we will work on that soon).

However, we need to create a wasm binary to upload the contract to blockchain. We can do it by passing an additional argument to the build command:

$ cargo build --target wasm32-unknown-unknown --release --lib

The --target argument tells cargo to perform cross-compilation for a given target instead of building a native binary for an OS it is running on - in this case, wasm32-unknown-unknown, which is a fancy name for Wasm target.

Contract would be properly created without --lib but later when we will add query it will be needed so it is a good idea to add this argument from the beginning.

Additionally, I passed the --release argument to the command - it is not required, but in most cases, debug information is not very useful while running on-chain. It is crucial to reduce the uploaded binary size for gas cost minimization. It is worth knowing that there is a CosmWasm Rust Optimizer tool that takes care of building even smaller binaries. For production, all the contracts should be compiled using this tool, but for learning purposes it is not an essential thing to do.

Aliasing build command

Now I can see you are disappointed in building your contracts with some overcomplicated command instead of simple cargo build. Hopefully, it is not the case. The common practice is to alias the building command to make it as simple as building a native app.

Let's create the .cargo/config file in your contract project directory with the following content:

[alias]
wasm = "build --target wasm32-unknown-unknown --release --lib"
wasm-debug = "build --target wasm32-unknown-unknown --lib"

Now, building your Wasm binary is as easy as executing cargo wasm. We also added the additional wasm-debug command for rare cases when we want to build the wasm binary, including debug information.

Checking contract validity

When the contract is built, the last step is to ensure it is a valid CosmWasm contract is to call cosmwasm-check on it:

$ cargo wasm
...
$ cosmwasm-check target/wasm32-unknown-unknown/release/contract.wasm
Available capabilities: {"cosmwasm_1_1", "iterator", "staking", "stargate"}

target/wasm32-unknown-unknown/release/contract.wasm: pass

All contracts (1) passed checks!

Basics

This chapter will go through creating basic smart contracts step by step. I will explain the core ideas behind CosmWasm and the typical contract structure.

Create a Rust project

As smart contracts are Rust library crates, we will start with creating one:

$ cargo new --lib ./contract

You created a simple Rust library, but it is not yet ready to be a smart contract. The first thing to do is to update the Cargo.toml file:

[package]
name = "contract"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
cosmwasm-std = { version = "1.1", features = ["staking"] }

As you can see, I added a crate-type field for the library section. Generating the cdylib is required to create a proper web assembly binary. The downside of this is that such a library cannot be used as a dependency for other Rust crates - for now, it is not needed, but later we will show how to approach reusing contracts as dependencies.

Additionally, we added one core dependency for smart contracts: the cosmwasm-std. This crate is a standard library for smart contracts. It provides essential utilities for communication with the outside world, helper functions, and types. Every smart contract we will build will use this dependency.

Entry points

Typical Rust application starts with the fn main() function called by the operating system. Smart contracts are not significantly different. When the message is sent to the contract, a function called "entry point" is called. Unlike native applications, which have only a single main entry point, smart contracts have a couple corresponding to different message types: instantiate, execute, query, sudo, migrate and more.

To start, we will go with three basic entry points:

  • instantiate is called once per smart contract lifetime - you can think about it as a constructor or initializer of a contract.
  • execute for handling messages which can modify contract state - they are used to perform some actual actions.
  • query for handling messages requesting some information from a contract; unlike execute, they can never affect any contract state, and are used just like database queries.

Go to your src/lib.rs file, and start with an instantiate entry point:

use cosmwasm_std::{entry_point, DepsMut, Empty, Env, MessageInfo, Response, StdResult};

#[entry_point]
pub fn instantiate(
    _deps: DepsMut,
    _env: Env,
    _info: MessageInfo,
    _msg: Empty,
) -> StdResult<Response> {
    Ok(Response::new())
}

In fact, instantiate is the only entry point required for a smart contract to be valid. It is not very useful in this form, but it is a start. Let's take a closer look at the entry point structure.

First, we import a couple of types for more consistent usage. Then we define our entry point. The instantiate takes four arguments:

  • deps: DepsMut is an utility type for communicating with the outer world - it allows querying, updating the contract state, querying other contracts state, and giving access to an Api object with a couple of helper functions for dealing with CW addresses.
  • env: Env is an object representing the blockchains state when executing the message - the chain height and id, current timestamp, and the called contract address.
  • info: MessageInfo contains metainformation about the message which triggered an execution - an address that sends the message, and chain native tokens sent with the message.
  • msg: Empty is the message triggering execution itself - for now, it is Empty type that represents {} JSON, but the type of this argument can be anything that is deserializable, and we will pass more complex types here in the future.

If you are new to the blockchain, those arguments may not have much sense to you, but while progressing through this book I will explain their usage one by one.

Notice an essential attribute decorating our entry point #[entry_point]. Its purpose is to wrap the whole entry point to the form Wasm runtime understands. The proper Wasm entry points can use only basic types supported natively by Wasm specification, and Rust structures and enums are not in this set. Working with such entry points would be rather overcomplicated, so CosmWasm creators delivered the entry_point macro. It creates the raw Wasm entry point, calling the decorated function internally and doing all the magic required to build our high-level Rust arguments from arguments passed by Wasm runtime.

The next thing to look at is the return type. I used StdResult<Response> for this simple example, which is an alias for Result<Response, StdError>. The return entry point type would always be a Result type, with some error type implementing ToString trait and a well-defined type for success case. For most entry points, an "Ok" case would be the Response type that allows fitting the contract into our actor model, which we will discuss very soon.

The body of the entry point is as simple as it could be - it always succeeds with a trivial empty response.

Generating first messages

We have already created a simple contract reacting to an Empty message on instantiate. Unfortunately, it is not very useful. Let's make it reactive.

Updating dependencies

First, we need to add sylvia and some other crates to our project.

[package]
name = "contract"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
cosmwasm-std = { version = "1.1", features = ["staking"] }
cosmwasm-schema = "1.1.6"
serde = { version = "1.0.147", features = ["derive"] }
sylvia = "0.2.1"
schemars = "0.8.11"

We had to add also some more dependencies required for generated by sylvia code to compile:

  • cosmwasm-schema - we will later rely on this dependency to generate the API schema of our contract.
  • serde - very important framework for serializing and deserializing Rust data structures. It is crucial as serialization is required for messages to be sent from and to the contract.
  • schemars - it will also be required later when dealing with generating the schema of our API.

Creating an instantiation message

For, this step we will create a new file:

  • src/contract.rs - here, we will define our messages and behavior of the contract upon receiving them

Add this module to src/lib.rs. You want it to be public, as users might want to get access to types stored inside your contract.

pub mod contract;

use cosmwasm_std::{entry_point, DepsMut, Empty, Env, MessageInfo, Response, StdResult};

#[entry_point]
pub fn instantiate(
    _deps: DepsMut,
    _env: Env,
    _info: MessageInfo,
    _msg: Empty,
) -> StdResult<Response> {
    Ok(Response::new())
}

Now let's create an instantiate method for our contract. In src/contract.rs

use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, StdResult};
use schemars;
use sylvia::contract;

pub struct AdminContract;

pub type ContractError = cosmwasm_std::StdError;

#[contract]
impl AdminContract {
    #[msg(instantiate)]
    pub fn instantiate(&self, _ctx: (DepsMut, Env, MessageInfo)) -> StdResult<Response> {
        Ok(Response::new())
    }
}

So what is going on here? First, we define the Admin struct. It is empty right now but later when we will learn about states and their fields will be used to store them. We introduce the alias ContractError, required by sylvia. Later we will create our custom ContractError, but for now, it is enough to just alias the cosmwasm_std::StdError. Notice it is marked as pub as we want it to be accessible in other modules. Next, we create an impl block for Admin and invoke the contract attribute macro from sylvia. It will generate a lot of boilerplate for us regarding messages generation and their dispatch. It will also make sure that none of the messages overlap and will catch it on compile time. Then there is a method instantiate which currently doesn't do much. What's important here is the #[msg(instantiate)]. contract macro will parse through the method and, based on its structure, generate InstantiateMsg with parameters equal to ones declared in instantiate method past _ctx. F.e., this will generate among others

impl AdminContract {
    pub fn instantiate(
        &self,
        _ctx: (DepsMut, Env, MessageInfo),
    ) -> StdResult<Response> {
        Ok(Response::new())
    }
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(
    sylvia::serde::Serialize,
    sylvia::serde::Deserialize,
    Clone,
    Debug,
    PartialEq,
    sylvia::schemars::JsonSchema,
)]
#[serde(rename_all = "snake_case")]
pub struct InstantiateMsg {}

impl InstantiateMsg {
    pub fn dispatch(
        self,
        contract: &AdminContract,
        ctx: (
            cosmwasm_std::DepsMut,
            cosmwasm_std::Env,
            cosmwasm_std::MessageInfo,
        ),
    ) -> StdResult<Response> {
        let Self {} = self;
        contract.instantiate(ctx.into()).map_err(Into::into)
    }
}

Let's focus on instantiate right now. As you can see, struct InstantiateMsg with all needed derives is being generated for you. Most important are Serialize and Deserialize There is also #[serde(rename_all = "snake_case")], which is not important for now.

Sylvia also generates a dispatch method for every msg linking it with user specified-behavior. One of its arguments is AdminContract, on which the message should be dispatched. Let's create the new method:

src/contract.rs

use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, StdResult};
use schemars;
use sylvia::contract;

pub struct AdminContract;

pub type ContractError = cosmwasm_std::StdError;

#[contract]
impl AdminContract {
    #[msg(instantiate)]
    pub fn instantiate(&self, _ctx: (DepsMut, Env, MessageInfo)) -> StdResult<Response> {
        Ok(Response::new())
    }
}

Great, now we can modify the src/lib.rs

pub mod contract;

use cosmwasm_std::{entry_point, DepsMut, Empty, Env, MessageInfo, Response, StdResult};

use crate::contract::{InstantiateMsg, AdminContract};

#[entry_point]
pub fn instantiate(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    msg: InstantiateMsg,
) -> StdResult<Response> {
    msg.dispatch(&AdminContract, (deps, env, info))
}

Empty is now changed to InstantiateMsg generated by sylvia. We dispatch the message, and it will trigger instatiation method. For now, it is just returning an empty Response, but let's change it in the following chapters, where we will introduce states and queries.

Contract state

We can instantiate our contract, but it doesn't do anything afterward. Let's make it more complex. In this chapter we will introduce the contracts state.

Adding contract state

We will initialize the on contract instantiation. The state will contain a list of admins who would be eligible to execute messages in the future.

The first thing to do is to update Cargo.toml with yet another dependency - the storage-plus crate with high-level bindings for CosmWasm smart contracts state management:

[package]
name = "contract"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
cosmwasm-std = { version = "1.1", features = ["staking"] }
cosmwasm-schema = "1.1.6"
serde = { version = "1.0.147", features = ["derive"] }
sylvia = "0.2.1"
schemars = "0.8.11"
cw-storage-plus = "1.0"

Now add the state as a field in your contract and instantiate it in the new method.

use cosmwasm_std::{Addr, DepsMut, Empty, Env, MessageInfo, Response, StdResult};
use cw_storage_plus::Map;
use schemars;
use sylvia::contract;

pub struct AdminContract<'a> {
    pub(crate) admins: Map<'a, &'a Addr, Empty>,
}

#[contract]
impl AdminContract<'_> {
    pub const fn new() -> Self {
        Self {
            admins: Map::new("admins"),
        }
    }

    #[msg(instantiate)]
    pub fn instantiate(
        &self,
        _ctx: (DepsMut, Env, MessageInfo),
    ) -> StdResult<Response> {
        Ok(Response::new())
    }
}

New types:

  • Map<_, _>

  • Addr - representation of actual address on a blockchain.

  • Empty - an empty struct that serves as a placeholder.

We declared state admins as immutable Map<'a, &'a Addr, Empty>. It might seem weird that we created Map with an Empty value containing no information. Still, our alternative would be to store it as Vec<Addr>, forcing us to load whole the Vec to alternate it or read a single element which would be a costly operation. Because of that, it is better to declare it as a Map.

But why isn't it mutable? How will we modify the elements?

The answer is tricky - this immutable is not keeping the state itself. The state is stored in the blockchain, and we can access it via the deps argument passed to entry points. The storage-plus constants are just accessor utilities helping us access this state in a structured way.

In CosmWasm, the blockchain state is just massive key-value storage. The keys are prefixed with metainformation pointing to the contract which owns them (so no other contract can alter them), but even after removing the prefixes, the single contract state is a smaller key-value pair.

storage-plus handles more complex state structures by additionally prefixing item keys intelligently. The key to the Map doesn't matter to us - it would be figured out to be unique based on a unique string passed to the new method.

Last new thing. We crated the new method for the AdminContract to hide the instantiation of the fields.

Initializing the state

Now that the state field has been added we can improve our instantiate. We will make it possible for a user to add new admins at contract instantiation.

use cosmwasm_std::{Addr, DepsMut, Empty, Env, MessageInfo, Response};
use cw_storage_plus::Map;
use schemars;
use sylvia::contract;

pub struct AdminContract<'a> {
   pub(crate) admins: Map<'a, &'a Addr, Empty>,
}

#[contract]
impl AdminContract<'_> {
   pub const fn new() -> Self {
       Self {
           admins: Map::new("admins"),
       }
   }
    ...

    #[msg(instantiate)]
    pub fn instantiate(
        &self,
        ctx: (DepsMut, Env, MessageInfo),
        admins: Vec<String>,
    ) -> StdResult<Response> {
        let (deps, _, _) = ctx;

        for admin in admins {
            let admin = deps.api.addr_validate(&admin)?;
            self.admins.save(deps.storage, &admin, &Empty {})?;
        }

        Ok(Response::new())
    }
}

Voila, that's all that is needed to update the state! With this change when we expand contract macro we should see:

#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(
    sylvia::serde::Serialize,
    sylvia::serde::Deserialize,
    Clone,
    Debug,
    PartialEq,
    sylvia::schemars::JsonSchema,
)]
#[serde(rename_all = "snake_case")]
pub struct InstantiateMsg {
    pub admins: Vec<String>,
}
impl InstantiateMsg {
    pub fn dispatch(
        self,
        contract: &AdminContract<'_>,
        ctx: (
            cosmwasm_std::DepsMut,
            cosmwasm_std::Env,
            cosmwasm_std::MessageInfo,
        ),
    ) -> StdResult<Response> {
        let Self { admins } = self;
        contract.instantiate(ctx.into(), admins).map_err(Into::into)
    }
}

As you can see, admins was set as a field of InstantiateMsg, and in dispatch, it's forwarded to instantiate implemented in our contract. There vector of strings is being transformed into a vector of Addr. We cannot take addresses as a message argument because only some strings are valid addresses. It might be confusing once we are working on tests. Any string could be used in the place of address.

Let me explain. Every string can be technically considered an address. However, not every string is an actual existing blockchain address. When we keep anything of type Addr in the contract, we assume it is a proper address in the blockchain. That is why the addr_validate function exits - to check this precondition.

Having data to store, we use the save function to write it into the contract state. Note that the first argument of save is &mut Storage, which is actual blockchain storage. As emphasized, the Map object stores nothing and is an accessor. It determines how to store the data in the storage given to it. The second argument is the serializable data to be stored, and the last one is the value which in our case is Empty.

With the state added to our contract, let's also update the entry_point. Go to src/lib.rs:

pub mod contract;

use cosmwasm_std::{entry_point, DepsMut, Empty, Env, MessageInfo, Response, StdResult};

use crate::contract::{InstantiateMsg, AdminContract};

const CONTRACT: AdminContract = AdminContract::new();

#[entry_point]
pub fn instantiate(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    msg: InstantiateMsg,
) -> StdResult<Response> {
    msg.dispatch(&CONTRACT, (deps, env, info))
}

Instead of passing the &AdminContract to the dispatch method, we first create the inner value CONTRACT by calling AdminContract::new().

Nice, we now have the state initialized on our contract, but we can't validate if the data is stored correctly. Let's change it in the next chapter, in which we will introduce query.

Creating a query

We can now initialize our contract and store some data in it. Let's write query to read it's content. We have already created a simple contract reacting to an empty instantiate message. Unfortunately, it is not very useful. Let's make it more reactive.

Declaring query response

Let's create a new file, src/responses.rs, containing responses to all the queries in our contract.

use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, schemars::JsonSchema, Debug, Default)]
pub struct AdminListResp {
    pub admins: Vec<String>,
}

We have here similar derives like in the case of InstantiateMsg. The most important ones are Serialize and Deserialize as we always want to return something serializable.

src/responses.rs is not part of our project, so let's change it. Go to src/lib.rs and add this module:

pub mod contract;
pub mod responses;

use cosmwasm_std::{entry_point, DepsMut, Empty, Env, MessageInfo, Response, StdResult};

use crate::contract::{InstantiateMsg, AdminContract};

const CONTRACT: AdminContract = AdminContract::new();

#[entry_point]
pub fn instantiate(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    msg: InstantiateMsg,
) -> StdResult<Response> {
    msg.dispatch(&CONTRACT, (deps, env, info))
}

Now that we have a response created, go to your src/contract.rs file and declare a new query.

use crate::responses::AdminListResp;
use cosmwasm_std::{Addr, Deps, DepsMut, Empty, Env, MessageInfo, Order, Response, StdResult};
use cw_storage_plus::Map;
use schemars;
use sylvia::contract;

pub struct AdminContract<'a> {
   pub(crate) admins: Map<'static, &'a Addr, Empty>,
}

#[contract]
impl AdminContract<'_> {
    ...

    #[msg(query)]
    pub fn admin_list(&self, ctx: (Deps, Env)) -> StdResult<AdminListResp> {
        let (deps, _) = ctx;

        let admins: Result<_, _> = self
            .admins
            .keys(deps.storage, None, None, Order::Ascending)
            .map(|addr| addr.map(String::from))
            .collect();

        Ok(AdminListResp { admins: admins? })
    }
}

With this done, we can expand our contract macro and see that QueryMsg is generated.

#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(
    sylvia::serde::Serialize,
    sylvia::serde::Deserialize,
    Clone,
    Debug,
    PartialEq,
    sylvia::schemars::JsonSchema,
    cosmwasm_schema::QueryResponses,
)]
#[serde(rename_all = "snake_case")]
pub enum QueryMsg {
    #[returns(AdminListResp)]
    AdminList {},
}
impl QueryMsg {
    pub fn dispatch(
        self,
        contract: &AdminContract,
        ctx: (cosmwasm_std::Deps, cosmwasm_std::Env),
    ) -> std::result::Result<sylvia::cw_std::Binary, ContractError> {
        use QueryMsg::*;
        match self {
            AdminList {} => {
                cosmwasm_std::to_binary(&contract.admin_list(ctx.into())?).map_err(Into::into)
            }
        }
    }
}

We will ignore #[returns(_)] and cosmwasm_schema::QueryResponses as they will be described later when we will talk about generating schema.

QueryMsg is an enum that will contain every query declared in your expanded impl. Thanks to that you can focus solely on defining the behavior of the contract on receiving a message, and you can leave it to sylvia to generate the messages and the dispatch.

Note that our enum has no type assigned to the only AdminList variant. Typically in Rust, we create such variants without additional {} after the variant name. Here the curly braces have a purpose. Without, them the variant would serialize to just a string type - so instead of { "admin_list": {} }, the JSON representation of this variant would be "admin_list".

Instead of returning the Response type on the success case, we return an arbitrary serializable object. It's because queries are not using a typical actor model message flow - they cannot trigger any actions nor communicate with other contracts in ways different than querying them (which is handled by the deps argument). The query always returns plain data, which should be presented directly to the querier. Sylvia does that by returning encoded response as Binary by calling to_binary function in dispatch.

In the case of query ctx is tuple of Deps and Env. We use Deps instead of DepsMut as we did in the case of instantiate because the query can never alter internal state of the smart contracts . It can only read the state. It comes with some consequences - for example, it is impossible to implement caching for future queries (as it would require some data cache to write to).

The other difference is the lack of the info argument. The reason here is that the entry point which performs actions (like instantiation or execution) can differ in how an action is performed based on the message metadata - for example, they can limit who can perform an action (and do so by checking the message sender). It is not a case for queries. Queries are purely to return some transformed contract state. It can be calculated based on chain metadata (so the state can "automatically" change after some time) but not on message info.

Now that QueryMsg is created, let's allow users to call it by defining the entry point for query in src/lib.rs.

pub mod contract;
pub mod responses;

use contract::{ContractError, ContractQueryMsg};
use cosmwasm_std::{entry_point, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult};

use crate::contract::{AdminContract, InstantiateMsg};

const CONTRACT: AdminContract = AdminContract::new();

#[entry_point]
pub fn instantiate(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    msg: InstantiateMsg,
) -> StdResult<Response> {
    msg.dispatch(&CONTRACT, (deps, env, info))
}

#[entry_point]
pub fn query(deps: Deps, env: Env, msg: ContractQueryMsg) -> Result<Binary, ContractError> {
    msg.dispatch(&CONTRACT, (deps, env))
}

There is one more new thing here. We were still talking about QueryMsg, but now we use ContractQueryMsg out of nowhere. Let me explain. Sylvia framework allows us to define interfaces. Users can create interfaces with specific functionalities and then implement them on contract. ContractQueryMsg is wrapper over QueryMsgs from a contract and it's interfaces which dispatch will call proper implementation. We will learn about Interfaces further in the book.

Now, when we have the contract ready to do something, let's go and test it.

Testing a query

Last time we created a new query. Now it is time to test it out. We will start with the basics - the unit test. This approach is simple and doesn't require much knowledge besides Rust. Go to the src/contract.rs and add a test at the bottom of the file:

use crate::responses::AdminListResp;
use cosmwasm_std::{Addr, Deps, DepsMut, Empty, Env, MessageInfo, Order, Response, StdResult};
use cw_storage_plus::Map;
use schemars;
use sylvia::contract;

pub struct AdminContract<'a> {
   pub(crate) admins: Map<'a, &'a Addr, Empty>,
}

#[contract]
impl AdminContract<'_> {
   pub const fn new() -> Self {
       Self {
           admins: Map::new("admins"),
       }
   }

   #[msg(instantiate)]
   pub fn instantiate(
       &self,
       ctx: (DepsMut, Env, MessageInfo),
       admins: Vec<String>,
   ) -> StdResult<Response> {
       let (deps, _, _) = ctx;

       for admin in admins {
           let admin = deps.api.addr_validate(&admin)?;
           self.admins.save(deps.storage, &admin, &Empty {})?;
       }

       Ok(Response::new())
   }

   #[msg(query)]
   pub fn admin_list(&self, ctx: (Deps, Env)) -> StdResult<AdminListResp> {
       let (deps, _) = ctx;

       let admins: Result<_, _> = self
           .admins
           .keys(deps.storage, None, None, Order::Ascending)
           .map(|addr| addr.map(String::from))
           .collect();

       Ok(AdminListResp { admins: admins? })
   }
}

#[cfg(test)]
mod tests {
    use crate::entry_points::{instantiate, query};
    use cosmwasm_std::from_binary;
    use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};

    use super::*;

    #[test]
    fn admin_list_query() {
        let mut deps = mock_dependencies();
        let env = mock_env();

        instantiate(
            deps.as_mut(),
            env.clone(),
            mock_info("sender", &[]),
            InstantiateMsg {
                admins: vec!["admin1".to_owned(), "admin2".to_owned()],
            },
        )
        .unwrap();

        let msg = QueryMsg::AdminList {};
        let resp = query(deps.as_ref(), env, ContractQueryMsg::AdminContract(msg)).unwrap();
        let resp: AdminListResp = from_binary(&resp).unwrap();
        assert_eq!(
            resp,
            AdminListResp {
                admins: vec!["admin1".to_owned(), "admin2".to_owned()],
            }
        );
    }
}

We have a simple flow here:

  • we instantiate contract with admins ["admin1", "admin2"],
  • we query the contract to see if both of them will be returned

Our instantiate, and query require deps and env as parameters. We will mock them with mock_dependencies and mock_env from comswasm-std.

You may notice the dependencies mock if of a type OwnedDeps instead of Deps, which we need here - this is why the as_ref function is called on it. If we needed a DepsMut object, we would use as_mut instead.

I extracted the deps and env to variables and passed them to calls. The idea is that those represent some blockchain persistent state, and we don't want to create them for every call. We want any changes to the contract state occurring in instantiate to be visible in the query. Also, we want to control how the environment differs in the query and instantiation.

The info argument is another story. The message info is unique for each message sent. To create the info mock, we must pass two arguments to the mock_info function.

First is the address performing a call. It may look strange to pass sender as an address instead of some mysterious wasm followed by hash, but it is a valid address. For testing purposes, such addresses are typically better, as they are way more verbose in case of failing tests.

The second argument is the funds that are sent with the message. For now, we leave it as an empty slice, as I don't want to talk about token transfers yet - we will cover it later.

So now it is more like a real-case scenario. I see just one problem. Nothing connects the instantiate call to the corresponding query. It seems that we assume there is some global contract. But if we would like to have two contracts instantiated differently in a single test case, it would become a mess. If only some tool could abstract this for us, wouldn't it be nice?

Introducing multitest

Let me introduce the multitest - library for creating tests for smart contracts in Rust.

The core idea of multitest is abstracting an entity of contract and simulating the blockchain environment for testing purposes. The purpose of this is to be able to test communication between smart contracts. It does its job well, but it is also an excellent tool for testing single-contract scenarios.

Update dependencies

First, we need to add a cw-multi-test to our Cargo.toml. We will also add anyhow. We will use it to bail on calls to unimplemented entry points like reply, migrate, and sudo.

[package]
name = "contract"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[features]
library = []

[dependencies]
cosmwasm-std = { version = "1.1", features = ["staking"] }
cosmwasm-schema = "1.1.6"
serde = { version = "1.0.147", features = ["derive"] }
sylvia = "0.2.1"
schemars = "0.8.11"
cw-storage-plus = "0.16.0"

[dev-dependencies]
anyhow = "1"
cw-multi-test = "0.16"

I added a new [dev-dependencies] section with dependencies not used by the final binary but which may be used by tools around the development process - for example, tests.

Creating a module for tests

Now we will create a new module, multitest. Let's first add it to the src/lib.rs

pub mod contract;
pub mod responses;

#[cfg(test)]
mod multitest;

use cosmwasm_std::{entry_point, Binary, Deps, DepsMut, Env, MessageInfo, Response};

use crate::contract::{AdminContract, ContractQueryMsg, InstantiateMsg};

const CONTRACT: AdminContract = AdminContract::new();

#[entry_point]
pub fn instantiate(
   deps: DepsMut,
   env: Env,
   info: MessageInfo,
   msg: InstantiateMsg,
) -> StdResult<Response> {
   msg.dispatch(&CONTRACT, (deps, env, info))
}

#[entry_point]
pub fn query(deps: Deps, env: Env, msg: ContractQueryMsg) -> StdResult<Binary> {
   msg.dispatch(&CONTRACT, (deps, env))
}

As this module is purely for testing purpose, we prefix it with #[cfg(test)].

Now create src/multitest.rs.

use anyhow::{bail, Result as AnyResult};
use cosmwasm_std::{from_slice, Empty};
use cw_multi_test::Contract;

use crate::contract::{AdminContract, ContractExecMsg, ContractQueryMsg, InstantiateMsg};

mod proxy;
mod tests;

impl Contract<Empty> for AdminContract<'_> {
    fn execute(
        &self,
        deps: cosmwasm_std::DepsMut<Empty>,
        env: cosmwasm_std::Env,
        info: cosmwasm_std::MessageInfo,
        msg: Vec<u8>,
    ) -> AnyResult<cosmwasm_std::Response<Empty>> {
        from_slice::<ContractExecMsg>(&msg)?
            .dispatch(self, (deps, env, info))
            .map_err(Into::into)
    }

    fn instantiate(
        &self,
        deps: cosmwasm_std::DepsMut<Empty>,
        env: cosmwasm_std::Env,
        info: cosmwasm_std::MessageInfo,
        msg: Vec<u8>,
    ) -> AnyResult<cosmwasm_std::Response<Empty>> {
        from_slice::<InstantiateMsg>(&msg)?
            .dispatch(self, (deps, env, info))
            .map_err(Into::into)
    }

    fn query(
        &self,
        deps: cosmwasm_std::Deps<Empty>,
        env: cosmwasm_std::Env,
        msg: Vec<u8>,
    ) -> AnyResult<cosmwasm_std::Binary> {
        from_slice::<ContractQueryMsg>(&msg)?
            .dispatch(self, (deps, env))
            .map_err(Into::into)
    }

    fn sudo(
        &self,
        _deps: cosmwasm_std::DepsMut<Empty>,
        _env: cosmwasm_std::Env,
        _msg: Vec<u8>,
    ) -> AnyResult<cosmwasm_std::Response<Empty>> {
        bail!("sudo not implemented for contract")
    }

    fn reply(
        &self,
        _deps: cosmwasm_std::DepsMut<Empty>,
        _env: cosmwasm_std::Env,
        _msg: cosmwasm_std::Reply,
    ) -> AnyResult<cosmwasm_std::Response<Empty>> {
        bail!("reply not implemented for contract")
    }

    fn migrate(
        &self,
        _deps: cosmwasm_std::DepsMut<Empty>,
        _env: cosmwasm_std::Env,
        _msg: Vec<u8>,
    ) -> AnyResult<cosmwasm_std::Response<Empty>> {
        bail!("reply not implemented for contract")
    }
}

So first, we added mod proxy and mod tests. We will create these files in the next step.

We impl Contract for AdminContract. This will allow us to use it in a cw-multi-test environment. To use it, we have to implement six entry points, but currently, our contract supports only two of them, instantiate and query. For unsupported entry points, we will call bail!. We will handle supported ones simmiliary as we did in src/lib.rs. The difference here is that interface Contract forces us to pass messages to entry points as binary slices. We can work with this by using from_slice. This function will parse binary slice to our message. We will then dispatch them as we did in src/lib.rs entry points and map_err in case any error is returned.

Prepare proxy

Now we will prepare a proxy for our contract. Our goal here is to remove repetitiveness and hide serialization from our tests. It will pay off after a while, but you will appreciate this approach as your contract grows,

Create src/multitest/proxy.rs and paste to it:

use cosmwasm_std::{Addr, StdResult};
use cw_multi_test::{App, Executor};

use crate::{
    contract::{AdminContract, InstantiateMsg, QueryMsg},
    responses::AdminListResp,
};

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct AdminContractCodeId(u64);

impl AdminContractCodeId {
    pub fn store_code(app: &mut App) -> Self {
        let code_id = app.store_code(Box::new(AdminContract::new()));
        Self(code_id)
    }

    #[track_caller]
    pub fn instantiate(
        self,
        app: &mut App,
        sender: &Addr,
        admins: Vec<String>,
        label: &str,
        admin: Option<String>,
    ) -> StdResult<AdminContractProxy> {
        let msg = InstantiateMsg { admins };

        app.instantiate_contract(self.0, sender.clone(), &msg, &[], label, admin)
            .map_err(|err| err.downcast().unwrap())
            .map(AdminContractProxy)
    }
}

#[derive(Debug)]
pub struct AdminContractProxy(Addr);

impl AdminContractProxy {
    #[track_caller]
    pub fn admin_list(&self, app: &App) -> StdResult<AdminListResp> {
        let msg = QueryMsg::AdminList {};

        app.wrap().query_wasm_smart(self.0.clone(), &msg)
    }
}

Two new structures here: AdminContractCodeId and AdminContractProxy. AdminContractCodeId will store u64 which represents the code id of our contract registered on a blockchain generated by store_code. We can use instantiate_contract with acquired code id and map address of received contract addr. It will on instantiate return the AdminContractProxy, which responsibility will be to send messages to our contract.

First multitest

Now we are ready to write our first multitest. Let's proceed with creating src/multitest/tests.rs.

use cosmwasm_std::Addr;
use cw_multi_test::App;

use crate::{multitest::proxy::AdminContractCodeId, responses::AdminListResp};

#[test]
fn basic() {
    let mut app = App::default();

    let owner = Addr::unchecked("addr0001");
    let admins = vec![
        "admin1".to_owned(),
        "admin2".to_owned(),
        "admin3".to_owned(),
    ];

    let code_id = AdminContractCodeId::store_code(&mut app);

    let contract = code_id
        .instantiate(&mut app, &owner, admins.clone(), "Cw20 contract", None)
        .unwrap();

    let resp = contract.admin_list(&app).unwrap();

    assert_eq!(resp, AdminListResp { admins });
}

We will first create default App. This will cache the state of our contract. We will create an owner of our contract using Addr::unchecked. Call store_code on our App to acquire code id and then init the contract which will return AdminContractProxy. We instantiate it with three admins, which we will then query using the admin_list method on the proxy. This should return to us AdminListResp with all three of them.

The test should pass, and we should have our first multitest. We will later expand it when we will have more functionality to test. Let's allow our contract to change its state using the execute message in next the chapter.

Execution messages

We created instantiate and query messages. We have the state in our contract and can test it. Now let's expand our contract by adding the possibility of updating the state. In this chapter, we will add the add_member execute message.

Custom error

Because we don't want non-admins to add new admins to our contract, we will have to take some steps to prevent it. In case of a call from non-admin we want to return an error that will inform the users that they are not authorized to perform this kind of operation on contract. We will achieve this goal by creating our custom error type. It will have to implement From trait to be compatible with most of the error cases in our contract. It will also have an Unauthorized variant.

First, let's update our Cargo.toml with a new dependency to thiserror.

[package]
name = "contract"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[features]
library = []

[dependencies]
cosmwasm-std = { version = "1.1", features = ["staking"] }
cosmwasm-schema = "1.1.6"
serde = { version = "1.0.147", features = ["derive"] }
sylvia = "0.2.1"
schemars = "0.8.11"
cw-storage-plus = "0.16.0"
thiserror = "1.0.37"

[dev-dependencies]
anyhow = "1"
cw-multi-test = "0.16"

This error provides us with a derive macro which, we will use to implement our ContractError. Let's add a new module to our src/lib.rs:

pub mod contract;
pub mod error;
pub mod responses;

#[cfg(test)]
mod multitest;

use contract::{ContractError, ContractQueryMsg};
use cosmwasm_std::{entry_point, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult};

use crate::contract::{AdminContract, InstantiateMsg};

const CONTRACT: AdminContract = AdminContract::new();

#[entry_point]
pub fn instantiate(
   deps: DepsMut,
   env: Env,
   info: MessageInfo,
   msg: InstantiateMsg,
) -> StdResult<Response> {
   msg.dispatch(&CONTRACT, (deps, env, info))
}

#[entry_point]
pub fn query(deps: Deps, env: Env, msg: ContractQueryMsg) -> Result<Binary, ContractError> {
   msg.dispatch(&CONTRACT, (deps, env))
}

And now, let's create src/error.rs:

use cosmwasm_std::{Addr, StdError};
use thiserror::Error;

#[derive(Error, Debug, PartialEq)]
pub enum ContractError {
    #[error("{0}")]
    Std(#[from] StdError),
    #[error("{sender} is not a contract admin")]
    Unauthorized { sender: Addr },
}

Our custom error will derive the following traits:

  • Error
  • Debug - for testing purposes
  • PartialEq - for testing purposes

#[error(_)] will generate Display implementation for our variants. In case of StdError we want only to forward the error message. In case of our custom Unauthorized variant user will receive information about who sent the message and why it failed.

Impl execute message

With the error created, let's implement the message. It will add a new admin if the sender is an admin.

use crate::error::ContractError;
use crate::responses::AdminListResp;
use cosmwasm_std::{Addr, Deps, DepsMut, Empty, Env, MessageInfo, Order, Response, StdResult};
use cw_storage_plus::Map;
use schemars;
use sylvia::contract;

pub struct AdminContract<'a> {
   pub(crate) admins: Map<'a, &'a Addr, Empty>,
}

#[contract]
impl AdminContract<'_> {
   pub const fn new() -> Self {
        Self {
           admins: Map::new("admins"),
       }
   }
    ...

    #[msg(instantiate)]
    pub fn instantiate(
        &self,
        ctx: (DepsMut, Env, MessageInfo),
        admins: Vec<String>,
    ) -> Result<Response, ContractError> {
        let (deps, _, _) = ctx;

        for admin in admins {
            let admin = deps.api.addr_validate(&admin)?;
            self.admins.save(deps.storage, &admin, &Empty {})?;
        }

        Ok(Response::new())
    }

   #[msg(query)]
   pub fn admin_list(&self, ctx: (Deps, Env)) -> StdResult<AdminListResp> {
       let (deps, _) = ctx;

       let admins: Result<_, _> = self
           .admins
           .keys(deps.storage, None, None, Order::Ascending)
           .map(|addr| addr.map(String::from))
           .collect();

       Ok(AdminListResp { admins: admins? })
   }

    #[msg(exec)]
    pub fn add_member(
        &self,
        ctx: (DepsMut, Env, MessageInfo),
        admin: String,
    ) -> Result<Response, ContractError> {
        let (deps, _, info) = ctx;

        if !self.admins.has(deps.storage, &info.sender) {
            return Err(ContractError::Unauthorized {
                sender: info.sender,
            });
        }
        let admin = deps.api.addr_validate(&admin)?;
        self.admins.save(deps.storage, &admin, &Empty {})?;

        Ok(Response::new())
    }
}

#[cfg(test)]
mod tests {
   use crate::entry_points::{instantiate, query};
   use cosmwasm_std::from_binary;
   use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};

   use super::*;

   #[test]
   fn admin_list_query() {
       let mut deps = mock_dependencies();
       let env = mock_env();

       instantiate(
           deps.as_mut(),
           env.clone(),
           mock_info("sender", &[]),
           InstantiateMsg {
               admins: vec!["admin1".to_owned(), "admin2".to_owned()],
           },
       )
       .unwrap();

       let msg = QueryMsg::AdminList {};
       let resp = query(deps.as_ref(), env, ContractQueryMsg::AdminContract(msg)).unwrap();
       let resp: AdminListResp = from_binary(&resp).unwrap();
       assert_eq!(
           resp,
           AdminListResp {
               admins: vec!["admin1".to_owned(), "admin2".to_owned()],
           }
       );
   }
}

First, let's add the ContractError to src/contract.rs and delete the old alias. We will update instantiate to return it instead of the StdError. In the case of a query it is mostly unnecessary as we rarely check anything in it, but if you have a reason you can also update it. It is a good approach to define your error type and return it in all but query messages.

To generate execute message we will prefix it with #[msg(exec)]. The return type is the same as in case of instantiate which is Result<Response, ContractError>. We will acquire the sender from MessageInfo. In case sender is unathorized a non-admin we will return its Addr in ContractError::Unauthorized. Because we can't be sure if the address sent by the admin is correct and represent the actual Addr in a blockchain we must first call the addr_validate on it. If it's correct we can save it and return Ok(Response).

Update entry points

Now that we have created the ExecMsg, let's add a new entry point. We have to do it only once per every type of message, thanks to the dispatch method.

pub mod contract;
pub mod error;
pub mod responses;

#[cfg(test)]
mod multitest;

use cosmwasm_std::{entry_point, Binary, Deps, DepsMut, Env, MessageInfo, Response};

use crate::contract::{AdminContract, ContractExecMsg, ContractQueryMsg, InstantiateMsg};
use crate::error::ContractError;

const CONTRACT: AdminContract = AdminContract::new();

#[entry_point]
pub fn instantiate(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    msg: InstantiateMsg,
) -> Result<Response, ContractError> {
    msg.dispatch(&CONTRACT, (deps, env, info))
}

#[entry_point]
pub fn query(deps: Deps, env: Env, msg: ContractQueryMsg) -> Result<Binary, ContractError> {
    msg.dispatch(&CONTRACT, (deps, env))
}

#[entry_point]
pub fn execute(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    msg: ContractExecMsg,
) -> Result<Response, ContractError> {
    msg.dispatch(&CONTRACT, (deps, env, info))
}

Nothing new here. We have the same deps, env, and info variables in the signature as in the case of instantiate. Our message is ContractExecMsg similar to ContractQueryMsg in case of the query. The body of the function is simply a dispatch call on the msg. We also updated instantiate to return ContractError.

Unit testing

Now let's add a simple unit test for execute message.

use crate::error::ContractError;
use crate::responses::AdminListResp;
use cosmwasm_std::{Addr, Deps, DepsMut, Empty, Env, MessageInfo, Order, Response, StdResult};
use cw_storage_plus::Map;
use schemars;
use sylvia::contract;

pub struct AdminContract<'a> {
   pub(crate) admins: Map<'a, &'a Addr, Empty>,
}

#[contract]
impl AdminContract<'_> {
   pub const fn new() -> Self {
       Self {
           admins: Map::new("admins"),
       }
   }

   #[msg(instantiate)]
   pub fn instantiate(
       &self,
       ctx: (DepsMut, Env, MessageInfo),
       admins: Vec<String>,
   ) -> Result<Response, ContractError> {
       let (deps, _, _) = ctx;

       for admin in admins {
           let admin = deps.api.addr_validate(&admin)?;
           self.admins.save(deps.storage, &admin, &Empty {})?;
       }

       Ok(Response::new())
   }

   #[msg(query)]
   pub fn admin_list(&self, ctx: (Deps, Env)) -> StdResult<AdminListResp> {
       let (deps, _) = ctx;

       let admins: Result<_, _> = self
           .admins
           .keys(deps.storage, None, None, Order::Ascending)
           .map(|addr| addr.map(String::from))
           .collect();

       Ok(AdminListResp { admins: admins? })
   }

   #[msg(exec)]
   pub fn add_member(
       &self,
       ctx: (DepsMut, Env, MessageInfo),
       admin: String,
   ) -> Result<Response, ContractError> {
       let (deps, _, info) = ctx;

       if !self.admins.has(deps.storage, &info.sender) {
           return Err(ContractError::Unauthorized {
               sender: info.sender,
           });
       }
       let admin = deps.api.addr_validate(&admin)?;

       self.admins.save(deps.storage, &admin, &Empty {})?;

       Ok(Response::new().add_attribute("action", "add_member"))
   }
}

#[cfg(test)]
mod tests {
    ...
    
   use crate::entry_points::{execute, instantiate, query};
   use cosmwasm_std::from_binary;
   use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};

   use super::*;

   #[test]
   fn admin_list_query() {
       let mut deps = mock_dependencies();
       let env = mock_env();

       instantiate(
           deps.as_mut(),
           env.clone(),
           mock_info("sender", &[]),
           InstantiateMsg {
               admins: vec!["admin1".to_owned(), "admin2".to_owned()],
           },
       )
       .unwrap();

       let msg = QueryMsg::AdminList {};
       let resp = query(deps.as_ref(), env, ContractQueryMsg::AdminContract(msg)).unwrap();
       let resp: AdminListResp = from_binary(&resp).unwrap();
       assert_eq!(
           resp,
           AdminListResp {
               admins: vec!["admin1".to_owned(), "admin2".to_owned()],
           }
       );
   }

    #[test]
    fn add_member() {
        let mut deps = mock_dependencies();
        let env = mock_env();

        instantiate(
            deps.as_mut(),
            env.clone(),
            mock_info("sender", &[]),
            InstantiateMsg {
                admins: vec!["admin1".to_owned(), "admin2".to_owned()],
            },
        )
        .unwrap();

        let info = mock_info("admin1", &[]);
        let msg = ExecMsg::AddMember {
            admin: "admin3".to_owned(),
        };
        execute(deps.as_mut(), env.clone(), info, ContractExecMsg::AdminContract(msg)).unwrap();

        let msg = QueryMsg::AdminList {};
        let resp = query(deps.as_ref(), env, ContractQueryMsg::AdminContract(msg)).unwrap();
        let resp: AdminListResp = from_binary(&resp).unwrap();
        assert_eq!(
            resp,
            AdminListResp {
                admins: vec!["admin1".to_owned(), "admin2".to_owned(), "admin3".to_owned()],
            }
        );
    }
}

It is very similar to the query test. The difference is the call to the execute entry point with ContractExecMsg. We created another mock_info instead of reusing one from instantiate because in a real life scenario MessageInfo is created for every message.

Multitest

We have ExecMsg created, the entry point is established for it and we have simple unit test checking if "Ok case" is working. Time to test it in the multitest enviroment. First, we will update our proxy. This time we only need to add a call to the add_member.

use cosmwasm_std::{Addr, StdResult};
use cw_multi_test::{App, AppResponse, Executor};

use crate::{
    contract::{AdminContract, ExecMsg, InstantiateMsg, QueryMsg},
    error::ContractError,
    responses::AdminListResp,
};

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct AdminContractCodeId(u64);

impl AdminContractCodeId {
   pub fn store_code(app: &mut App) -> Self {
       let code_id = app.store_code(Box::new(AdminContract::new()));
       Self(code_id)
   }

   #[track_caller]
   pub fn instantiate(
       self,
       app: &mut App,
       sender: &Addr,
       admins: Vec<String>,
       label: &str,
       admin: Option<String>,
   ) -> Result<AdminContractProxy, ContractError> {
       let msg = InstantiateMsg { admins };

       app.instantiate_contract(self.0, sender.clone(), &msg, &[], label, admin)
           .map_err(|err| err.downcast().unwrap())
           .map(AdminContractProxy)
   }
}

#[derive(Debug)]
pub struct AdminContractProxy(Addr);

impl AdminContractProxy {
   #[track_caller]
   pub fn admin_list(&self, app: &App) -> StdResult<AdminListResp> {
       let msg = QueryMsg::AdminList {};

       app.wrap().query_wasm_smart(self.0.clone(), &msg)
   }

    #[track_caller]
    pub fn add_member(
        &self,
        app: &mut App,
        sender: &Addr,
        admin: String,
    ) -> Result<AppResponse, ContractError> {
        let msg = ExecMsg::AddMember { admin };

        app.execute_contract(sender.clone(), self.0.clone(), &msg, &[])
            .map_err(|err| err.downcast().unwrap())
    }
}

You can see that App will return a new type AppResponse from cw_multi_test rather then Response. As for the body of this method, it is important to pass sender as execute_contract requires it. We will again pass the empty slice as funds as we don't want to deal with it for now. We call map_err here, trying to downcast the error to ContractError.

Now that proxy is ready, let's add a new multitest. Our scenario will be an error case. An unauthorized user will try to add themselves as an admin to the contract, which should fail.

use cosmwasm_std::Addr;
use cw_multi_test::App;

use crate::error::ContractError;
use crate::{multitest::proxy::AdminContractCodeId, responses::AdminListResp};

#[test]
fn basic() {
   let mut app = App::default();

   let owner = Addr::unchecked("addr0001");
   let admin1 = Addr::unchecked("admin1");
   let admin2 = Addr::unchecked("admin2");
   let admin3 = Addr::unchecked("admin3");

   let code_id = AdminContractCodeId::store_code(&mut app);

   let contract = code_id
       .instantiate(
           &mut app,
           &owner,
           vec![admin1.to_string(), admin2.to_string()],
           "Cw20 contract",
           None,
       )
       .unwrap();

   let resp = contract.admin_list(&app).unwrap();

   assert_eq!(
       resp,
       AdminListResp {
           admins: vec![admin1.to_string(), admin2.to_string()]
       }
   );

   contract
       .add_member(&mut app, &admin1, admin3.to_string())
       .unwrap();

   let resp = contract.admin_list(&app).unwrap();

   assert_eq!(
       resp,
       AdminListResp {
           admins: vec![admin1.to_string(), admin2.to_string(), admin3.to_string()]
       }
   );
}

#[test]
fn unathorized() {
    let mut app = App::default();

    let owner = Addr::unchecked("addr0001");
    let admin1 = Addr::unchecked("admin1");
    let admin2 = Addr::unchecked("admin2");
    let admin3 = Addr::unchecked("admin3");

    let code_id = AdminContractCodeId::store_code(&mut app);

    let contract = code_id
        .instantiate(
            &mut app,
            &owner,
            vec![admin1.to_string(), admin2.to_string()],
            "Cw20 contract",
            None,
        )
        .unwrap();

    let resp = contract.admin_list(&app).unwrap();

    assert_eq!(
        resp,
        AdminListResp {
            admins: vec![admin1.to_string(), admin2.to_string()]
        }
    );

    let err = contract
        .add_member(&mut app, &admin3, admin3.to_string())
        .unwrap_err();

    assert_eq!(err, ContractError::Unauthorized { sender: admin3 });

    let resp = contract.admin_list(&app).unwrap();

    assert_eq!(
        resp,
        AdminListResp {
            admins: vec![admin1.to_string(), admin2.to_string()]
        }
    );
}

Once again, as in the case of the unit test this is similiar to the query test. After contract instantiation, we will call add_member with admin3 as a sender and catch the error using unwrap_err. Then we will use assert_eq to check if it is ContractError::Unauthorized { sender: "admin3" }. In the end, we will query the contract for a list of admins, and admin3 is not on the list.

Great our contract works as expected, but we could test more scenarios. I encourage you to think of other edge cases and try to test them by yourself. We can now add new admins to the contract, but some might want to leave this responsibility. Try to add new message leave and don't forget to test the new functionality.

Events attributes and data

The only way our contract can communicate to the world, for now, is by queries. Smart contracts are passive - they cannot invoke any action by themselves. They can do it only as a reaction to a call. But if you tried playing with wasmd, you know that execution on the blockchain can return some metadata.

There are two things the contract can return to the caller: events and data. Events are something produced by almost every real-life smart contract. In contrast, data is rarely used, designed for contract-to-contract communication.

Returning events

As an example, we would add an event admin_added emitted by our contract on the execution of add_member:

use crate::error::ContractError;
use crate::responses::AdminListResp;
use cosmwasm_std::{
    Addr, Deps, DepsMut, Empty, Env, Event, MessageInfo, Order, Response, StdResult,
};
use cw_storage_plus::Map;
use schemars;
use sylvia::contract;

pub struct AdminContract<'a> {
   pub(crate) admins: Map<'a, &'a Addr, Empty>,
}

#[contract]
impl AdminContract<'_> {
   pub const fn new() -> Self {
       Self {
           admins: Map::new("admins"),
       }
   }

   #[msg(instantiate)]
   pub fn instantiate(
       &self,
       ctx: (DepsMut, Env, MessageInfo),
       admins: Vec<String>,
   ) -> Result<Response, ContractError> {
       let (deps, _, _) = ctx;

       for admin in admins {
           let admin = deps.api.addr_validate(&admin)?;
           self.admins.save(deps.storage, &admin, &Empty {})?;
       }

       Ok(Response::new())
   }

   #[msg(query)]
   pub fn admin_list(&self, ctx: (Deps, Env)) -> StdResult<AdminListResp> {
       let (deps, _) = ctx;

       let admins: Result<_, _> = self
           .admins
           .keys(deps.storage, None, None, Order::Ascending)
           .map(|addr| addr.map(String::from))
           .collect();

       Ok(AdminListResp { admins: admins? })
   }

    ...

    #[msg(exec)]
    pub fn add_member(
        &self,
        ctx: (DepsMut, Env, MessageInfo),
        admin: String,
    ) -> Result<Response, ContractError> {
        let (deps, _, info) = ctx;

        if !self.admins.has(deps.storage, &info.sender) {
            return Err(ContractError::Unauthorized {
                sender: info.sender,
            });
        }
        let admin = deps.api.addr_validate(&admin)?;
        self.admins.save(deps.storage, &admin, &Empty {})?;

        let resp = Response::new()
            .add_attribute("action", "add_member")
            .add_event(Event::new("admin_added").add_attribute("addr", admin));
        Ok(resp)
    }
}

#[cfg(test)]
mod tests {
   use crate::entry_points::{execute, instantiate, query};
   use cosmwasm_std::from_binary;
   use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};

   use super::*;

   #[test]
   fn admin_list_query() {
       let mut deps = mock_dependencies();
       let env = mock_env();

       instantiate(
           deps.as_mut(),
           env.clone(),
           mock_info("sender", &[]),
           InstantiateMsg {
               admins: vec!["admin1".to_owned(), "admin2".to_owned()],
           },
       )
       .unwrap();

       let msg = QueryMsg::AdminList {};
       let resp = query(deps.as_ref(), env, ContractQueryMsg::AdminContract(msg)).unwrap();
       let resp: AdminListResp = from_binary(&resp).unwrap();
       assert_eq!(
           resp,
           AdminListResp {
               admins: vec!["admin1".to_owned(), "admin2".to_owned()],
           }
       );
   }

   #[test]
   fn add_member() {
       let mut deps = mock_dependencies();
       let env = mock_env();

       instantiate(
           deps.as_mut(),
           env.clone(),
           mock_info("sender", &[]),
           InstantiateMsg {
               admins: vec!["admin1".to_owned(), "admin2".to_owned()],
           },
       )
       .unwrap();

       let info = mock_info("admin1", &[]);
       let msg = ExecMsg::AddMember {
           admin: "admin3".to_owned(),
       };
       execute(
           deps.as_mut(),
           env.clone(),
           info,
           ContractExecMsg::AdminContract(msg),
       )
       .unwrap();

       let msg = QueryMsg::AdminList {};
       let resp = query(deps.as_ref(), env, ContractQueryMsg::AdminContract(msg)).unwrap();
       let resp: AdminListResp = from_binary(&resp).unwrap();
       assert_eq!(
           resp,
           AdminListResp {
               admins: vec![
                   "admin1".to_owned(),
                   "admin2".to_owned(),
                   "admin3".to_owned()
               ],
           }
       );
   }
}

An event is built from two things: an event type provided in the new function and attributes. Attributes are added to an event with the add_attributes or the add_attribute call. Attributes are key-value pairs. Because an event cannot contain any list, to achieve reporting multiple similar actions taking place, we need to emit multiple small events instead of a collective one.

Events are emitted by adding them to the response with add_event or add_events call. Additionally, there is a possibility to add attributes directly to the response. It is just sugar. By default, every execution emits a standard "wasm" event. Adding attributes to the result adds them to the default event.

We can check if events are properly emitted by contract. It is not always done, as it is much of boilerplate in test, but events are, generally, more like logs - not necessarily considered the main contract logic.

Due to the extra event attribute being added by contract, let's add a new method to the proxy:

use cosmwasm_std::{Addr, StdResult};
use cw_multi_test::{App, AppResponse, Executor};

use crate::{
    contract::{AdminContract, ExecMsg, InstantiateMsg, QueryMsg},
    error::ContractError,
    responses::AdminListResp,
};

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct AdminContractCodeId(u64);

impl AdminContractCodeId {
   pub fn store_code(app: &mut App) -> Self {
       let code_id = app.store_code(Box::new(AdminContract::new()));
       Self(code_id)
   }

   #[track_caller]
   pub fn instantiate(
       self,
       app: &mut App,
       sender: &Addr,
       admins: Vec<String>,
       label: &str,
       admin: Option<String>,
   ) -> Result<AdminContractProxy, ContractError> {
       let msg = InstantiateMsg { admins };

       app.instantiate_contract(self.0, sender.clone(), &msg, &[], label, admin)
           .map_err(|err| err.downcast().unwrap())
           .map(AdminContractProxy)
   }
}

#[derive(Debug)]
pub struct AdminContractProxy(Addr);

impl AdminContractProxy {
    pub fn addr(&self) -> &Addr {
        &self.0
    }
   #[track_caller]
   pub fn admin_list(&self, app: &App) -> StdResult<AdminListResp> {
       let msg = QueryMsg::AdminList {};

       app.wrap().query_wasm_smart(self.0.clone(), &msg)
   }

   #[track_caller]
   pub fn add_member(
       &self,
       app: &mut App,
       sender: &Addr,
       admin: String,
   ) -> Result<AppResponse, ContractError> {
       let msg = ExecMsg::AddMember { admin };

       app.execute_contract(sender.clone(), self.0.clone(), &msg, &[])
           .map_err(|err| err.downcast().unwrap())
   }
}

This will return the address of the contract on a blockchain which is, by default, added to the events attribute and which we will need in our assertion. Now let's update our basic multitest checking if execution emits events:

use cosmwasm_std::{Addr, Event};
use cw_multi_test::App;

use crate::error::ContractError;
use crate::{multitest::proxy::AdminContractCodeId, responses::AdminListResp};

#[test]
fn basic() {
    let mut app = App::default();

    let owner = Addr::unchecked("addr0001");
    let admin1 = Addr::unchecked("admin1");
    let admin2 = Addr::unchecked("admin2");
    let admin3 = Addr::unchecked("admin3");

    let code_id = AdminContractCodeId::store_code(&mut app);

    let contract = code_id
        .instantiate(
            &mut app,
            &owner,
            vec![admin1.to_string(), admin2.to_string()],
            "Cw20 contract",
            None,
        )
        .unwrap();

    let resp = contract.admin_list(&app).unwrap();

    assert_eq!(
        resp,
        AdminListResp {
            admins: vec![admin1.to_string(), admin2.to_string()]
        }
    );

    let resp = contract
        .add_member(&mut app, &admin1, admin3.to_string())
        .unwrap();

    let wasm = resp.events.iter().find(|ev| ev.ty == "wasm").unwrap();
    assert_eq!(
        wasm.attributes
            .iter()
            .find(|attr| attr.key == "action")
            .unwrap()
            .value,
        "add_member"
    );

    let admin_added: Vec<_> = resp
        .events
        .iter()
        .filter(|ev| ev.ty == "wasm-admin_added")
        .collect();
    assert_eq!(admin_added[0], &Event::new("wasm-admin_added").add_attribute("_contract_addr", contract.addr()).add_attribute("addr", Addr::unchecked("admin3")));

    let resp = contract.admin_list(&app).unwrap();

    assert_eq!(
        resp,
        AdminListResp {
            admins: vec![admin1.to_string(), admin2.to_string(), admin3.to_string()]
        }
    );
}

#[test]
fn unathorized() {
   let mut app = App::default();

   let owner = Addr::unchecked("addr0001");
   let admin1 = Addr::unchecked("admin1");
   let admin2 = Addr::unchecked("admin2");
   let admin3 = Addr::unchecked("admin3");

   let code_id = AdminContractCodeId::store_code(&mut app);

   let contract = code_id
       .instantiate(
           &mut app,
           &owner,
           vec![admin1.to_string(), admin2.to_string()],
           "Cw20 contract",
           None,
       )
       .unwrap();

   let resp = contract.admin_list(&app).unwrap();

   assert_eq!(
       resp,
       AdminListResp {
           admins: vec![admin1.to_string(), admin2.to_string()]
       }
   );

   let err = contract
       .add_member(&mut app, &admin3, admin3.to_string())
       .unwrap_err();

   assert_eq!(err, ContractError::Unauthorized { sender: admin3 });

   let resp = contract.admin_list(&app).unwrap();

   assert_eq!(
       resp,
       AdminListResp {
           admins: vec![admin1.to_string(), admin2.to_string()]
       }
   );
}

If you have prepared an Ok scenario test after the previous chapter, then you can update it as above. I have added the add_member call in the basic test and captured the response in the resp variable.

As you can see, testing events on a simple test made it clunky. First of all, every string is heavily string-based - a lack of type control makes writing such tests difficult. Also, event types are prefixed with "wasm-" - it may not be a huge problem, but it doesn't clarify verification. But the problem is, how layered event structures are, which makes verifying them tricky. Also, the "wasm" event is particularly tricky, as it contains an implied attribute - _contract_addr containing an address called a contract. My general rule is - do not test emitted events unless some logic depends on them.

Data

Besides events, any smart contract execution may produce a data object. In contrast to events, data can be structured. It makes it a way better choice to perform any communication logic to rely on it. On the other hand, it turns out it is very rarely helpful outside of contract-to-contract communication. Data is always only one single object on the response, which is set using the set_data function. Because of its low usefulness in a single contract environment, we will not spend time on it right now - an example of it will be covered later when contract-to-contract communication will be discussed. Until then, it is just helpful to know such an entity exists.

Dealing with funds

When you hear smart contracts, you think of blockchain. When you hear blockchain, you often think of cryptocurrencies. It is not the same, but crypto assets, or as we often call them: tokens, are very closely connected to the blockchain. CosmWasm has a notion of a native token. Native tokens are assets managed by the blockchain core instead of smart contracts. Often such assets have some special meaning, like being used for paying gas fees or staking for consensus algorithm, but can be just arbitrary assets.

Native tokens are assigned to their owners but can be transferred by their nature. Everything had an address in the blockchain is eligible to have its native tokens. As a consequence - tokens can be assigned to smart contracts! Every message sent to the smart contract can have some funds sent with it. In this chapter, we will take advantage of that and create a way to reward the hard work performed by admins. We will create a new message - Donate, which anyone can use to donate some funds to admins, divided equally.

Preparing messages

Traditionally we need to prepare our messages. We need to create a new ExecuteMsg variant, but we will also modify the Instantiate message a bit - we need to have some way of defining the name of a native token we would use for donations. It would be possible to allow users to send any tokens they want, but we want to simplify things for now.

use crate::error::ContractError;
use crate::responses::AdminListResp;
use cosmwasm_std::{
    Addr, Deps, DepsMut, Empty, Env, Event, MessageInfo, Order, Response, StdResult,
};
use cw_storage_plus::{Map, Item};
use schemars;
use sylvia::contract;

pub struct AdminContract<'a> {
    pub(crate) admins: Map<'a, &'a Addr, Empty>,
    pub(crate) donation_denom: Item<'a, String>,
}

#[contract]
impl AdminContract<'_> {
    pub const fn new() -> Self {
        Self {
            admins: Map::new("admins"),
            donation_denom: Item::new("donation_denom"),
        }
    }

    #[msg(instantiate)]
    pub fn instantiate(
        &self,
        ctx: (DepsMut, Env, MessageInfo),
        admins: Vec<String>,
        donation_denom: String,
    ) -> Result<Response, ContractError> {
        let (deps, _, _) = ctx;

        for admin in admins {
            let admin = deps.api.addr_validate(&admin)?;
            self.admins.save(deps.storage, &admin, &Empty {})?;
        }
        self.donation_denom.save(deps.storage, &donation_denom)?;
        Ok(Response::new())
    }

   #[msg(query)]
   pub fn admin_list(&self, ctx: (Deps, Env)) -> StdResult<AdminListResp> {
       let (deps, _) = ctx;

       let admins: Result<_, _> = self
           .admins
           .keys(deps.storage, None, None, Order::Ascending)
           .map(|addr| addr.map(String::from))
           .collect();

       Ok(AdminListResp { admins: admins? })
   }

   #[msg(exec)]
   pub fn add_member(
       &self,
       ctx: (DepsMut, Env, MessageInfo),
       admin: String,
   ) -> Result<Response, ContractError> {
       let (deps, _, info) = ctx;

       if !self.admins.has(deps.storage, &info.sender) {
           return Err(ContractError::Unauthorized {
               sender: info.sender,
           });
       }
       let admin = deps.api.addr_validate(&admin)?;
       let resp = Response::new().add_attribute("action", "add_member");
       self.admins.save(deps.storage, &admin, &Empty {})?;
       let resp = resp.add_event(Event::new("admin_added").add_attribute("addr", admin));
       Ok(resp)
   }

   #[msg(exec)]
   pub fn leave(&self, ctx: (DepsMut, Env, MessageInfo)) -> Result<Response, ContractError> {
       let (deps, _, info) = ctx;

       self.admins.remove(deps.storage, &info.sender);

       Ok(Response::new().add_attribute("action", "leave"))
   }
}

#[cfg(test)]
mod tests {
   use crate::entry_points::{execute, instantiate, query};
   use cosmwasm_std::from_binary;
   use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};

   use super::*;

   const ATOM: &str = "atom";

   #[test]
   fn admin_list_query() {
       let mut deps = mock_dependencies();
       let env = mock_env();

       instantiate(
           deps.as_mut(),
           env.clone(),
           mock_info("sender", &[]),
           InstantiateMsg {
               admins: vec!["admin1".to_owned(), "admin2".to_owned()],
               donation_denom: ATOM.to_owned(),
           },

       )
       .unwrap();

       let msg = QueryMsg::AdminList {};
       let resp = query(deps.as_ref(), env, ContractQueryMsg::AdminContract(msg)).unwrap();
       let resp: AdminListResp = from_binary(&resp).unwrap();
       assert_eq!(
           resp,
           AdminListResp {
               admins: vec!["admin1".to_owned(), "admin2".to_owned()],
           }
       );
   }

   #[test]
   fn add_member() {
       let mut deps = mock_dependencies();
       let env = mock_env();

       instantiate(
           deps.as_mut(),
           env.clone(),
           mock_info("sender", &[]),
           InstantiateMsg {
               admins: vec!["admin1".to_owned(), "admin2".to_owned()],
               donation_denom: ATOM.to_owned(),
           },
       )
       .unwrap();

       let info = mock_info("admin1", &[]);
       let msg = ExecMsg::AddMember {
           admin: "admin3".to_owned(),
       };
       execute(
           deps.as_mut(),
           env.clone(),
           info,
           ContractExecMsg::AdminContract(msg),
       )
       .unwrap();

       let msg = QueryMsg::AdminList {};
       let resp = query(deps.as_ref(), env, ContractQueryMsg::AdminContract(msg)).unwrap();
       let resp: AdminListResp = from_binary(&resp).unwrap();
       assert_eq!(
           resp,
           AdminListResp {
               admins: vec![
                   "admin1".to_owned(),
                   "admin2".to_owned(),
                   "admin3".to_owned()
               ],
           }
       )
   }
}

We have added a new state donation_denom, which is of type Item. A user has to pass a new value to instantiate the contract. I will let you fix tests, which should at this point fail due to missing parameter.

Let's update our Cargo.toml with a new dependency to cw-utils.

[package]
name = "contract"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[features]
library = []

[dependencies]
cosmwasm-std = { version = "1.1", features = ["staking"] }
cosmwasm-schema = "1.1.6"
serde = { version = "1.0.147", features = ["derive"] }
sylvia = "0.2.1"
schemars = "0.8.11"
thiserror = "1.0.37"
cw-storage-plus = "0.16.0"
cw-utils = "0.16"

[dev-dependencies]
anyhow = "1"
cw-multi-test = "0.16"

Now let's implement the new donate message.

use crate::error::ContractError;
use crate::responses::AdminListResp;
use cosmwasm_std::{
    Addr, Deps, DepsMut, Empty, Env, Event, MessageInfo, Order, Response, StdResult, StdError, BankMsg, coins,
};
use cw_storage_plus::{Map, Item};
use schemars;
use sylvia::contract;

pub struct AdminContract<'a> {
    pub(crate) admins: Map<'a, &'a Addr, Empty>,
    pub(crate) donation_denom: Item<'a, String>,
}

#[contract]
impl AdminContract<'_> {
   pub const fn new() -> Self {
       Self {
           admins: Map::new("admins"),
           donation_denom: Item::new("donation_denom"),
       }
   }

   #[msg(instantiate)]
   pub fn instantiate(
       &self,
       ctx: (DepsMut, Env, MessageInfo),
       admins: Vec<String>,
       donation_denom: String,
   ) -> Result<Response, ContractError> {
       let (deps, _, _) = ctx;

       for admin in admins {
           let admin = deps.api.addr_validate(&admin)?;
           self.admins.save(deps.storage, &admin, &Empty {})?;
       }
       self.donation_denom.save(deps.storage, &donation_denom)?;
       Ok(Response::new())
   }

   #[msg(query)]
   pub fn admin_list(&self, ctx: (Deps, Env)) -> StdResult<AdminListResp> {
       let (deps, _) = ctx;

       let admins: Result<_, _> = self
           .admins
           .keys(deps.storage, None, None, Order::Ascending)
           .map(|addr| addr.map(String::from))
           .collect();

       Ok(AdminListResp { admins: admins? })
   }

   #[msg(exec)]
   pub fn add_member(
       &self,
       ctx: (DepsMut, Env, MessageInfo),
       admin: String,
   ) -> Result<Response, ContractError> {
       let (deps, _, info) = ctx;

       if !self.admins.has(deps.storage, &info.sender) {
           return Err(ContractError::Unauthorized {
               sender: info.sender,
           });
       }
       let admin = deps.api.addr_validate(&admin)?;
       let resp = Response::new().add_attribute("action", "add_member");
       self.admins.save(deps.storage, &admin, &Empty {})?;
       let resp = resp.add_event(Event::new("admin_added").add_attribute("addr", admin));
       Ok(resp)
   }

   #[msg(exec)]
   pub fn leave(&self, ctx: (DepsMut, Env, MessageInfo)) -> Result<Response, ContractError> {
       let (deps, _, info) = ctx;

       self.admins.remove(deps.storage, &info.sender);

       Ok(Response::new().add_attribute("action", "leave"))
   }

    ...
    #[msg(exec)]
    pub fn donate(&self, ctx: (DepsMut, Env, MessageInfo)) -> Result<Response, ContractError> {
        let (deps, _, info) = ctx;

        let denom = self.donation_denom.load(deps.storage)?;
        let admins_len = self
            .admins
            .keys(deps.storage, None, None, Order::Ascending)
            .filter_map(|admin| admin.ok())
            .count();

        let donation = cw_utils::must_pay(&info, &denom)
            .map_err(|err| StdError::generic_err(err.to_string()))?
            .u128();

        let donation_per_admin = donation / (admins_len as u128);

        let admins = self
            .admins
            .keys(deps.storage, None, None, Order::Ascending)
            .filter_map(|admin| admin.ok());

        let messages = admins.into_iter().map(|admin| BankMsg::Send {
            to_address: admin.to_string(),
            amount: coins(donation_per_admin, &denom),
        });

        let resp = Response::new()
            .add_messages(messages)
            .add_attribute("action", "donate")
            .add_attribute("amount", donation.to_string())
            .add_attribute("per_admin", donation_per_admin.to_string());

        Ok(resp)
    }
}

#[cfg(test)]
mod tests {
   use crate::entry_points::{execute, instantiate, query};
   use cosmwasm_std::from_binary;
   use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};

   use super::*;

   const ATOM: &str = "atom";

   #[test]
   fn admin_list_query() {
       let mut deps = mock_dependencies();
       let env = mock_env();

       instantiate(
           deps.as_mut(),
           env.clone(),
           mock_info("sender", &[]),
           InstantiateMsg {
               admins: vec!["admin1".to_owned(), "admin2".to_owned()],
               donation_denom: ATOM.to_owned(),
           },

       )
       .unwrap();

       let msg = QueryMsg::AdminList {};
       let resp = query(deps.as_ref(), env, ContractQueryMsg::AdminContract(msg)).unwrap();
       let resp: AdminListResp = from_binary(&resp).unwrap();
       assert_eq!(
           resp,
           AdminListResp {
               admins: vec!["admin1".to_owned(), "admin2".to_owned()],
           }
       );
   }

   #[test]
   fn add_member() {
       let mut deps = mock_dependencies();
       let env = mock_env();

       instantiate(
           deps.as_mut(),
           env.clone(),
           mock_info("sender", &[]),
           InstantiateMsg {
               admins: vec!["admin1".to_owned(), "admin2".to_owned()],
               donation_denom: ATOM.to_owned(),
           },
       )
       .unwrap();

       let info = mock_info("admin1", &[]);
       let msg = ExecMsg::AddMember {
           admin: "admin3".to_owned(),
       };
       execute(
           deps.as_mut(),
           env.clone(),
           info,
           ContractExecMsg::AdminContract(msg),
       )
       .unwrap();

       let msg = QueryMsg::AdminList {};
       let resp = query(deps.as_ref(), env, ContractQueryMsg::AdminContract(msg)).unwrap();
       let resp: AdminListResp = from_binary(&resp).unwrap();
       assert_eq!(
           resp,
           AdminListResp {
               admins: vec![
                   "admin1".to_owned(),
                   "admin2".to_owned(),
                   "admin3".to_owned()
               ],
           }
       );
   }
}

Sending the funds to another contract is performed by adding bank messages to the response. The blockchain would expect any message which is returned in contract response as a part of an execution. This design is related to an actor model implemented by CosmWasm. The whole actor model will be described in detail later. For now, you can assume this is a way to handle token transfers. Before sending tokens to admins, we have to calculate the amount of dotation per admin. It is done by searching funds for an entry describing our donation token and dividing the number of tokens sent by the number of admins. Note that because the integral division is always rounding down.

As a consequence, it is possible that not all tokens sent as a donation would end up with no admins accounts. Any leftover would be left on our contract account forever. There are plenty of ways of dealing with this issue - figuring out one of them would be a great exercise.

The last missing part is updating the ContractError - the must_pay call returns a PaymentError which we can't convert to our error type yet:

use cosmwasm_std::{Addr, StdError};
use cw_utils::PaymentError;
use thiserror::Error;

#[derive(Error, Debug, PartialEq)]
pub enum ContractError {
    #[error("{0}")]
    Std(#[from] StdError),
    #[error("{sender} is not a contract admin")]
    Unauthorized { sender: Addr },
    #[error("Payment error: {0}")]
    Payment(#[from] PaymentError)
}

As you can see, to handle incoming funds, I used the utility function - I encourage you to take a look at its implementation - this would give you a good understanding of how incoming funds are structured in MessageInfo.

Now it's time to check if the funds are distributed correctly. The way for that is to write a test. First let's update src/multitest/proxy.rs

use cosmwasm_std::{Addr, Coin, StdResult};
use cw_multi_test::{App, AppResponse, Executor};

use crate::{
    contract::{AdminContract, ExecMsg, InstantiateMsg, QueryMsg},
    error::ContractError,
    responses::AdminListResp,
};

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct AdminContractCodeId(u64);

impl AdminContractCodeId {
   pub fn store_code(app: &mut App) -> Self {
       let code_id = app.store_code(Box::new(AdminContract::new()));
       Self(code_id)
   }

   #[track_caller]
   pub fn instantiate(
       self,
       app: &mut App,
       sender: &Addr,
       admins: Vec<String>,
       donation_denom: String,
       label: &str,
       admin: Option<String>,
   ) -> Result<AdminContractProxy, ContractError> {
       let msg = InstantiateMsg {
           admins,
           donation_denom,
       };

       app.instantiate_contract(self.0, sender.clone(), &msg, &[], label, admin)
           .map_err(|err| err.downcast().unwrap())
           .map(AdminContractProxy)
   }
}

#[derive(Debug)]
pub struct AdminContractProxy(Addr);

impl AdminContractProxy {
   pub fn addr(&self) -> &Addr {
       &self.0
   }
   #[track_caller]
   pub fn admin_list(&self, app: &App) -> StdResult<AdminListResp> {
       let msg = QueryMsg::AdminList {};

       app.wrap().query_wasm_smart(self.0.clone(), &msg)
   }

   #[track_caller]
   pub fn add_member(
       &self,
       app: &mut App,
       sender: &Addr,
       admin: String,
   ) -> Result<AppResponse, ContractError> {
       let msg = ExecMsg::AddMember { admin };

       app.execute_contract(sender.clone(), self.0.clone(), &msg, &[])
           .map_err(|err| err.downcast().unwrap())
   }

   #[track_caller]
   pub fn leave(&self, app: &mut App, sender: &Addr) -> Result<AppResponse, ContractError> {
       let msg = ExecMsg::Leave {};

       app.execute_contract(sender.clone(), self.0.clone(), &msg, &[])
           .map_err(|err| err.downcast().unwrap())
   }

    #[track_caller]
    pub fn donate(
        &self,
        app: &mut App,
        sender: &Addr,
        funds: &[Coin],
    ) -> Result<AppResponse, ContractError> {
        let msg = ExecMsg::Donate {};

        app.execute_contract(sender.clone(), self.0.clone(), &msg, &funds)
            .map_err(|err| err.downcast().unwrap())
    }
}

Now let' add donate test in src/multitest/tests.rs

use cosmwasm_std::{coins, Addr, Event};
use cw_multi_test::App;

use crate::error::ContractError;
use crate::{multitest::proxy::AdminContractCodeId, responses::AdminListResp};

const ATOM: &str = "atom";

#[test]
fn basic() {
   let mut app = App::default();

   let owner = Addr::unchecked("addr0001");
   let admin1 = Addr::unchecked("admin1");
   let admin2 = Addr::unchecked("admin2");
   let admin3 = Addr::unchecked("admin3");

   let code_id = AdminContractCodeId::store_code(&mut app);

   let contract = code_id
       .instantiate(
           &mut app,
           &owner,
           vec![admin1.to_string(), admin2.to_string()],
           ATOM.to_string(),
           "Cw20 contract",
           None,
       )
       .unwrap();

   let resp = contract.admin_list(&app).unwrap();

   assert_eq!(
       resp,
       AdminListResp {
           admins: vec![admin1.to_string(), admin2.to_string()]
       }
   );

   let resp = contract
       .add_member(&mut app, &admin1, admin3.to_string())
       .unwrap();

   let wasm = resp.events.iter().find(|ev| ev.ty == "wasm").unwrap();
   assert_eq!(
       wasm.attributes
           .iter()
           .find(|attr| attr.key == "action")
           .unwrap()
           .value,
       "add_member"
   );

   let admin_added: Vec<_> = resp
       .events
       .iter()
       .filter(|ev| ev.ty == "wasm-admin_added")
       .collect();
   assert_eq!(
       admin_added[0],
       &Event::new("wasm-admin_added")
           .add_attribute("_contract_addr", contract.addr())
           .add_attribute("addr", Addr::unchecked("admin3"))
   );

   let resp = contract.admin_list(&app).unwrap();

   assert_eq!(
       resp,
       AdminListResp {
           admins: vec![admin1.to_string(), admin2.to_string(), admin3.to_string()]
       }
   );
}

#[test]
fn unathorized() {
   let mut app = App::default();

   let owner = Addr::unchecked("addr0001");
   let admin1 = Addr::unchecked("admin1");
   let admin2 = Addr::unchecked("admin2");
   let admin3 = Addr::unchecked("admin3");

   let code_id = AdminContractCodeId::store_code(&mut app);

   let contract = code_id
       .instantiate(
           &mut app,
           &owner,
           vec![admin1.to_string(), admin2.to_string()],
           ATOM.to_string(),
           "Cw20 contract",
           None,
       )
       .unwrap();

   let resp = contract.admin_list(&app).unwrap();

   assert_eq!(
       resp,
       AdminListResp {
           admins: vec![admin1.to_string(), admin2.to_string()]
       }
   );

   let err = contract
       .add_member(&mut app, &admin3, admin3.to_string())
       .unwrap_err();

   assert_eq!(err, ContractError::Unauthorized { sender: admin3 });

   let resp = contract.admin_list(&app).unwrap();

   assert_eq!(
       resp,
       AdminListResp {
           admins: vec![admin1.to_string(), admin2.to_string()]
       }
   );
}

#[test]
fn leave() {
   let mut app = App::default();

   let owner = Addr::unchecked("addr0001");
   let admin1 = Addr::unchecked("admin1");
   let admin2 = Addr::unchecked("admin2");

   let code_id = AdminContractCodeId::store_code(&mut app);

   let contract = code_id
       .instantiate(
           &mut app,
           &owner,
           vec![admin1.to_string(), admin2.to_string()],
           ATOM.to_string(),
           "Cw20 contract",
           None,
       )
       .unwrap();

   let resp = contract.admin_list(&app).unwrap();

   assert_eq!(
       resp,
       AdminListResp {
           admins: vec![admin1.to_string(), admin2.to_string()]
       }
   );

   contract.leave(&mut app, &admin1).unwrap();

   let resp = contract.admin_list(&app).unwrap();

   assert_eq!(
       resp,
       AdminListResp {
           admins: vec![admin2.to_string()]
       }
   );
}

#[test]
fn donate() {
    let owner = Addr::unchecked("addr0001");
    let admin1 = Addr::unchecked("admin1");
    let admin2 = Addr::unchecked("admin2");

    let mut app = App::new(|router, _, storage| {
        router
            .bank
            .init_balance(storage, &owner, coins(5, ATOM))
            .unwrap()
    });

    let code_id = AdminContractCodeId::store_code(&mut app);

    let contract = code_id
        .instantiate(
            &mut app,
            &owner,
            vec![admin1.to_string(), admin2.to_string()],
            ATOM.to_string(),
            "Cw20 contract",
            None,
        )
        .unwrap();

    contract.donate(&mut app, &owner, &coins(5, ATOM)).unwrap();

    assert_eq!(
        app.wrap().query_balance(owner, ATOM).unwrap().amount.u128(),
        0
    );

    assert_eq!(
        app.wrap()
            .query_balance(contract.addr(), ATOM)
            .unwrap()
            .amount
            .u128(),
        1
    );

    assert_eq!(
        app.wrap()
            .query_balance(admin1, ATOM)
            .unwrap()
            .amount
            .u128(),
        2
    );

    assert_eq!(
        app.wrap()
            .query_balance(admin2, ATOM)
            .unwrap()
            .amount
            .u128(),
        2
    );
}

Fairly simple. I don't particularly appreciate that every balance check is eight lines of code, but it can be improved by enclosing this assertion into a separate function, probably with the #[track_caller] attribute.

The critical thing to talk about is how app creation changed. Because we need some initial tokens on an owner account, instead of using the default constructor, we have to provide it with an initializer function. Unfortunately, new documentation is not easy to follow - even if a function is not very complicated. What it takes as an argument is a closure with three arguments - the Router with all modules supported by multi-test, the API object, and the state. This function is called once during contract instantiation. The router object contains some generic fields - we are interested in the bank in particular. It has a type of BankKeeper, where the init_balance function sits.

Plot Twist!

As we covered most of the important basics about building Rust smart contracts, I have a serious exercise for you.

The contract we built has an exploitable bug. All donations are distributed equally across admins. However, every admin is eligible to add another admin. And nothing is preventing the admin from adding himself to the list and receiving twice as many rewards as others!

Try to write a test that detects such a bug, then fix it and ensure the bug never more occurs.

Even if the admin cannot add the same address to the list, he can always create new accounts and add them, but this is something unpreventable on the contract level, so do not prevent that. Handling this kind of case is done by properly designing whole applications, which is out of this chapter's scope.

Good practices

All the relevant basics are covered. Now let's talk about some good practices.

JSON renaming

Due to Rust style, all our message variants are spelled in a camel case. It is standard practice, but it has a drawback - all messages are serialized and deserialized by the serde using those variants names. The problem is that it is more common to use snake cases for field names in the JSON world. Luckily there is an effortless way to tell the serde, to change the names casing for serialization purposes. I mentioned it earlier when talking about query messages - #[serde(rename_all = "snake_case")]. Sylvia will automatically generate it for you in case of messages. Unfortunately, in case of responses to your messages, you will have to do it by yourself. Let's update our response with this attribute:

use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, schemars::JsonSchema, Debug, Default)]
#[serde(rename_all = "snake_case")]
pub struct AdminListResp {
    pub admins: Vec<String>,
}

Looking at our AdminListResp, you might argue that all these derive look too clunky, and I agree. Luckily the cosmwasm-schema create delivers cw_serde macro, which we can use to reduce a boilerplate:

use cosmwasm_schema::cw_serde;

#[cw_serde]
pub struct AdminListResp {
    pub admins: Vec<String>,
}

JSON schema

Talking about JSON API, it is worth mentioning JSON Schema. It is a way of defining the shape of JSON messages. It is a good practice to provide a way to generate schemas for contract API. The problem is that writing JSON schemas by hand is a pain. The good news is that there is a crate that would help us with that. We have already used it before, and it is called schemars. Sylvia will force you to add this derive to your responses and will generate messages with it.

The only thing missing is new a crate-type in our Cargo.toml:

[package]
name = "contract"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib", "rlib"]

[features]
library = []

[dependencies]
cosmwasm-std = { version = "1.1", features = ["staking"] }
cosmwasm-schema = "1.1.6"
serde = { version = "1.0.147", features = ["derive"] }
sylvia = "0.2.1"
schemars = "0.8.11"
thiserror = "1.0.37"
cw-storage-plus = "0.16.0"
cw-utils = "0.16"

[dev-dependencies]
anyhow = "1"
cw-multi-test = "0.16"

I added rlib. cdylib crates cannot be used as typical Rust dependencies. As a consequence, it is impossible to create examples for such crates.

The next step is to create a tool generating actual schemas. We will do it by creating a binary in our crate. Create a new bin/schema.rs file:

use contract::contract::{ContractExecMsg, ContractQueryMsg, InstantiateMsg};
use cosmwasm_schema::write_api;

fn main() {
    write_api! {
        instantiate: InstantiateMsg,
        execute: ContractExecMsg,
        query: ContractQueryMsg,
    }
}

Cargo is smart enough to recognize files in the src/bin directory as utility binaries for the crate. Now we can generate our schemas:

cargo run schema
   Finished dev [unoptimized + debuginfo] target(s) in 0.03s
     Running `target/debug/schema schema`
Exported the full API as /home/janw/workspace/confio/sylvia-book-contract/schema/contract.json

I encourage you to go to generated file to see what the schema looks like.

The problem is that unfortunately creating this binary makes our project fail to compile on the Wasm target - which is the most important in the end. Hopefully, we don't need to build the schema binary for the Wasm target - let's align the .cargo/config file:

[alias]
wasm = "build --target wasm32-unknown-unknown --release --lib"
wasm-debug = "build --target wasm32-unknown-unknown --lib"
schema = "run schema"

The --lib flag added to wasm cargo aliases tells the toolchain to build only the library target - it would skip building any binaries. Additionally, I added the convenience schema alias to generate schema calling simply cargo schema.

If you are using cw-utils in version 1.0 cargo wasm command will still fail because of the dependency to getrandom crate. To fix the wasm compilation, you have to add yet another dependency to our Cargo.toml:

[package]
name = "contract"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
cosmwasm-std = { version = "1.1", features = ["staking"] }
cosmwasm-schema = "1.1.6"
serde = { version = "1.0.147", features = ["derive"] }
sylvia = "0.2.1"
schemars = "0.8.11"
cw-storage-plus = "1.0"
thiserror = "1.0.37"
cw-utils = "1.0"
getrandom = { version = "0.2", features = ["js"] }

[dev-dependencies]
anyhow = "1"
cw-multi-test = "0.16"

With this last tweak, cargo wasm should compile correctly.

Disabling entry points for libraries

Since we added the rlib target for the contract, it is, as mentioned before, usable as a dependency. The problem is that the contract depending on ours, would have Wasm entry points generated twice - once in the dependency and once in the final contract. We can work this around by disabling generating Wasm entry points for the contract if the crate is used as a dependency. We would use feature flags for that.

Start with updating Cargo.toml:

[package]
name = "contract"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib", "rlib"]

[features]
library = []

[dependencies]
cosmwasm-std = { version = "1.1", features = ["staking"] }
cosmwasm-schema = "1.1.6"
serde = { version = "1.0.147", features = ["derive"] }
sylvia = "0.2.1"
schemars = "0.8.11"
thiserror = "1.0.37"
cw-storage-plus = "0.16.0"
cw-utils = "0.16"
getrandom = { version = "0.2", features = ["js"] }

[dev-dependencies]
anyhow = "1"
cw-multi-test = "0.16"

This way, we created a new feature flag for our crate. Now we want to disable the entry_point attribute if our contract would be used as a dependency. We will do it by a slight update of src/lib.rs:

pub mod contract;
pub mod error;
pub mod responses;

#[cfg(test)]
mod multitest;

#[cfg(not(feature = "library"))]
mod entry_points {
    use cosmwasm_std::{entry_point, Binary, Deps, DepsMut, Env, MessageInfo, Response};

    use crate::contract::{AdminContract, ContractExecMsg, ContractQueryMsg, InstantiateMsg};
    use crate::error::ContractError;

    const CONTRACT: AdminContract = AdminContract::new();

    #[entry_point]
    pub fn instantiate(
        deps: DepsMut,
        env: Env,
        info: MessageInfo,
        msg: InstantiateMsg,
    ) -> Result<Response, ContractError> {
        msg.dispatch(&CONTRACT, (deps, env, info))
    }

    #[entry_point]
    pub fn query(deps: Deps, env: Env, msg: ContractQueryMsg) -> Result<Binary, ContractError> {
        msg.dispatch(&CONTRACT, (deps, env))
    }

    #[entry_point]
    pub fn execute(
        deps: DepsMut,
        env: Env,
        info: MessageInfo,
        msg: ContractExecMsg,
    ) -> Result<Response, ContractError> {
        msg.dispatch(&CONTRACT, (deps, env, info))
    }
}

The cfg_attr is a conditional compilation attribute, similar to the cfg we used before for the test. It expands to the given attribute if the condition expands to true. In our case - it would expand to nothing if the feature "library" is enabled, or it would expand just to #[entry_point] in another case.

Since now to add this contract as a dependency, don't forget to enable the feature like this:

[dependencies]
my_contract = { version = "0.1", features = ["library"] }

Floating point types

Now you are ready to create smart contracts on your own. It is time to discuss an important limitation of CosmWasm smart contracts - floating-point numbers.

The story is short: you cannot use floating-point types in smart contracts. Never. CosmWasm virtual machine on purpose does not implement floating-point Wasm instructions, even such basics as F32Load. The reasoning is simple: they are not safe to work with in the blockchain world.

The biggest problem is that contract will compile, but uploading it to the blockchain would fail with an error message claiming there is a floating-point operation in the contract. A tool that verifies if the contract is valid (it does not contain any fp operations but also has all needed entry points and so on) is called cosmwasm-check.

This limitation has two implications. First, you always have to use decimals of fixed-point arithmetic in your contracts. It is not a problem, considering that cosmwasm-std provides you with the Decimal and Decimal256 types.

The other implication is tricky - you must be careful with the crates you use. In particular, one gotcha in the serde crate - deserialization of usize type is using floating-point operations. That means you can never use usize (or isize) types in your deserialized messages in the contract.

Another thing that will not work with serde is untagged enums deserialization. The workaround is to create custom deserialization of such enums using serde-cw-value crate. It is a fork of serde-value crate which avoids generating floating-point instructions.

Reusability

We have covered almost everything needed to write CosmWasm smart contracts with sylvia. In this last chapter of the basics section, I will tell you about the ability to define interfaces in sylvia.

Problem

Let's say that after creating this contract we start working on another. While planning its implementation, we notice that its functionality is just a superset of our AdminContract. We could copy all the code to our new contract, but it's creating unnecessary redundancy and would force us to maintain multiple implementations of the same functionality.

Solution

Sylvia has a feature to reuse already implemented messages and apply them in new contracts. Clone and open sylvia repository. Go to contracts/cw1-subkeys/src/contract.rs. You can notice that the impl block for the Cw1SubkeysContract is preceded with #[messages(...)] attribute.

#[contract]
#[messages(cw1 as Cw1)]
#[messages(whitelist as Whitelist)]
impl Cw1SubkeysContract<'_> {
    ...
}

contract macro considers both interfaces marked as messages, which in our case are cw1 and whitelist. It then generates ContractQueryMsg and ContractExecMsg as such:

#[allow(clippy::derive_partial_eq_without_eq)]
#[serde(rename_all = "snake_case", untagged)]
pub enum ContractQueryMsg {
    Cw1(cw1::Cw1QueryMsg),
    Whitelist(whitelist::WhitelistQueryMsg),
    Cw1SubkeysContract(QueryMsg),
}

impl ContractQueryMsg {
    pub fn dispatch(
        self,
        contract: &Cw1SubkeysContract,
        ctx: (cosmwasm_std::Deps, cosmwasm_std::Env),
    ) -> std::result::Result<sylvia::cw_std::Binary, ContractError> {
        const _: () = {
            let msgs: [&[&str]; 3usize] = [
                &cw1::Cw1QueryMsg::messages(),
                &whitelist::WhitelistQueryMsg::messages(),
                &QueryMsg::messages(),
            ];
            sylvia::utils::assert_no_intersection(msgs);
        };
        match self {
            ContractQueryMsg::Cw1(msg) => msg.dispatch(contract, ctx),
            ContractQueryMsg::Whitelist(msg) => msg.dispatch(contract, ctx),
            ContractQueryMsg::Cw1SubkeysContract(msg) => msg.dispatch(contract, ctx),
        }
    }
}

We can finally see why we need these ContractQueryMsg and ContractExecMsg next to our regular message enums. Sylvia generated three tuple variants:

  • Cw1 - which contains query msg defined in whitelist;

  • Whitelist- which contains query msg defined in cw1;

  • Cw1SubkeysContract - which contains query msg defined in our contract.

We use this wrapper to match with the proper variant and then call dispatch on this message. Sylvia also ensure that no message overlaps between interfaces and contract so that contracts API won't break.

Declaring interface

How are the interface messages implemented? Cw1SubkeysContract is an excellent example because it presents two situations:

  • Cw1 - declares a set of functionality that should be supported in implementing this interface contract and forces the user to define behavior for them;
  • Whitelist - same as above, but being primarily implemented for the Cw1Whitelist contract, it has already implementation defined.

For the latter one, we can either implement it ourselves or reuse it as it was done in contract/cw1-subkeys/src/whitelist.rs. As you can see, we only call a method on whitelist forwarding the arguments passed to the contract. To see the implementation, you can go to contract/cw1-whitelist/src/whitelist.rs. The interface has to be defined as a trait with a call to macro interface. It currently supports only execute and query messages. In this case, it is right away implemented on the Cw1Whitelist contract, and this implementation is being reused in contract/cw1-subkeys/src/whitelist.rs.

You might also want to separate the functionalities of your contract in some sets. It is the case of Cw1. It is created as a separate crate and reused in both Cw1WhitelistContract and Cw1SubkeysContract. You can check the implementation in contracts/cw1-subkeys/src/cw1.rs. For interface declaration itself, take a look at contracts/cw1/src/lib.rs.

Practice

We now have enough background to create an interface ourselves. Let's say we started working on some other contract and found out that the donate functionality would fit in it very well. Open our AdminContract project. We will first create src/donation.rs:

use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, StdError};
use sylvia::interface;

#[interface]
pub trait Donation {
    type Error: From<StdError>;

    #[msg(exec)]
    fn donate(&self, ctx: (DepsMut, Env, MessageInfo)) -> Result<Response, Self::Error>;
}

We add the interface attribute macro to the newly created trait Donation. If we stopped here, we will get an error. interface forces us to alias Error for the trait. We will declare it as From<StdError> as it is the error from cosmwasm_std, and we will always want our errors to implement From on it. We then declare donate returning Result with Self::Error, allowing users to define their error types.

Now that the declaration is ready, let's move the donate implementation to this file:

use cosmwasm_std::{coins, BankMsg, DepsMut, Env, MessageInfo, Order, Response, StdError};
use sylvia::interface;

use crate::{contract::AdminContract, error::ContractError};

#[interface]
pub trait Donation {
    type Error: From<StdError>;

    #[msg(exec)]
    fn donate(&self, ctx: (DepsMut, Env, MessageInfo)) -> Result<Response, Self::Error>;
}

impl Donation for AdminContract<'_> {
    type Error = ContractError;

    fn donate(&self, ctx: (DepsMut, Env, MessageInfo)) -> Result<Response, ContractError> {
        let (deps, _, info) = ctx;

        let denom = self.donation_denom.load(deps.storage)?;
        let admins_len = self
            .admins
            .keys(deps.storage, None, None, Order::Ascending)
            .filter_map(|admin| admin.ok())
            .count();

        let donation = cw_utils::must_pay(&info, &denom)
            .map_err(|err| StdError::generic_err(err.to_string()))?
            .u128();

        let donation_per_admin = donation / (admins_len as u128);

        let admins = self
            .admins
            .keys(deps.storage, None, None, Order::Ascending)
            .filter_map(|admin| admin.ok());

        let messages = admins.into_iter().map(|admin| BankMsg::Send {
            to_address: admin.to_string(),
            amount: coins(donation_per_admin, &denom),
        });

        let resp = Response::new()
            .add_messages(messages)
            .add_attribute("action", "donate")
            .add_attribute("amount", donation.to_string())
            .add_attribute("per_admin", donation_per_admin.to_string());

        Ok(resp)
    }
}

We alias Error as ContractError and move the whole method from src/contract.rs. Now we only need to add the Donation interface to our contract:

use cosmwasm_std::{
    Addr, Deps, DepsMut, Empty, Env, Event, MessageInfo, Order, Response, StdResult,
};
use cw_storage_plus::{Item, Map};
use schemars;
use sylvia::contract;

use crate::{donation, error::ContractError, responses::AdminListResp};

pub struct AdminContract<'a> {
   pub(crate) admins: Map<'a, &'a Addr, Empty>,
   pub(crate) donation_denom: Item<'a, String>,
}

#[contract]
#[messages(donation as Donation)]
impl AdminContract<'_> {
    ...
   pub const fn new() -> Self {
       AdminContract {
           admins: Map::new("admins"),
           donation_denom: Item::new("donation_denom"),
       }
   }

   #[msg(instantiate)]
   pub fn instantiate(
       &self,
       ctx: (DepsMut, Env, MessageInfo),
       admins: Vec<String>,
       donation_denom: String,
   ) -> Result<Response, ContractError> {
       let (deps, _, _) = ctx;

       for admin in admins {
           let admin = deps.api.addr_validate(&admin)?;
           self.admins.save(deps.storage, &admin, &Empty {})?;
       }
       self.donation_denom.save(deps.storage, &donation_denom)?;

       Ok(Response::new())
   }

   #[msg(query)]
   pub fn admin_list(&self, ctx: (Deps, Env)) -> StdResult<AdminListResp> {
       let (deps, _) = ctx;

       let admins: Result<_, _> = self
           .admins
           .keys(deps.storage, None, None, Order::Ascending)
           .map(|addr| addr.map(String::from))
           .collect();

       Ok(AdminListResp { admins: admins? })
   }

   #[msg(exec)]
   pub fn add_member(
       &self,
       ctx: (DepsMut, Env, MessageInfo),
       admin: String,
   ) -> Result<Response, ContractError> {
       let (deps, _, info) = ctx;

       if !self.admins.has(deps.storage, &info.sender) {
           return Err(ContractError::Unauthorized {
               sender: info.sender,
           });
       }
       let admin = deps.api.addr_validate(&admin)?;
       self.admins.save(deps.storage, &admin, &Empty {})?;

       let resp = Response::new()
           .add_attribute("action", "add_member")
           .add_event(Event::new("admin_added").add_attribute("addr", admin));
       Ok(resp)
   }
}

#[cfg(test)]
mod tests {
   use crate::entry_points::{execute, instantiate, query};
   use cosmwasm_std::from_binary;
   use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};

   use super::*;

   #[test]
   fn admin_list_query() {
       let mut deps = mock_dependencies();
       let env = mock_env();

       instantiate(
           deps.as_mut(),
           env.clone(),
           mock_info("sender", &[]),
           InstantiateMsg {
               admins: vec!["admin1".to_owned(), "admin2".to_owned()],
               donation_denom: "atom".to_owned(),
           },
       )
       .unwrap();

       let msg = QueryMsg::AdminList {};
       let resp = query(deps.as_ref(), env, ContractQueryMsg::AdminContract(msg)).unwrap();
       let resp: AdminListResp = from_binary(&resp).unwrap();
       assert_eq!(
           resp,
           AdminListResp {
               admins: vec!["admin1".to_owned(), "admin2".to_owned()],
           }
       );
   }

   #[test]
   fn add_member() {
       let mut deps = mock_dependencies();
       let env = mock_env();

       instantiate(
           deps.as_mut(),
           env.clone(),
           mock_info("sender", &[]),
           InstantiateMsg {
               admins: vec!["admin1".to_owned(), "admin2".to_owned()],
               donation_denom: "atom".to_owned(),
           },
       )
       .unwrap();

       let info = mock_info("admin1", &[]);
       let msg = ExecMsg::AddMember {
           admin: "admin3".to_owned(),
       };
       execute(
           deps.as_mut(),
           env.clone(),
           info,
           ContractExecMsg::AdminContract(msg),
       )
       .unwrap();

       let msg = QueryMsg::AdminList {};
       let resp = query(deps.as_ref(), env, ContractQueryMsg::AdminContract(msg)).unwrap();
       let resp: AdminListResp = from_binary(&resp).unwrap();
       assert_eq!(
           resp,
           AdminListResp {
               admins: vec![
                   "admin1".to_owned(),
                   "admin2".to_owned(),
                   "admin3".to_owned()
               ],
           }
       );
   }
}

Now you should have a compilation error while running tests. src/multitest/proxy.rs used ExecMsg::Donate {}, but it is not a part of ExecMsg defined in src/contract.rs. You can fix it f.e. by changing it to let msg = crate::donation::ExecMsg::Donate {}; or you can alias it in the use statement and use it as DonateExecMsg::Donate {}.

Now all tests should pass. You can run cargo schema to check what changed in your contract API.

Actor model

This section describes the fundaments of CosmWasm smart contracts architecture, which determines how do they communicate with each other. I want to go through this before teaching step by step how to create multiple contracts relating to each other, to give you a grasp of what to expect. Don't worry if it will not be clear after the first read - I suggest going through this chapter once now and maybe giving it another take in the future when you know the practical part of this.

The whole thing described here is officially documented in the SEMANTICS.md, of the cosmwasm repository.

Idea behind an Actor Model

The actor model is the solution to the problem of communication between smart contracts. Let's take a look at the reasons why this particular solution is chosen in CosmWasm, and what are the consequences of that.

The problem

Smart contracts can be imagined as sandboxed microservices. Due to SOLID principles, it is valuable to split responsibilities between entities. However, to split the work between contracts themselves, there is a need to communicate between them, so if one contract is responsible for managing group membership, it is possible to call its functionality from another contract.

The traditional way to solve this problem in SW engineering is to model services as functions that would be called with some RPC mechanism, and return its result as a response. Even though this approach looks nice, it creates sort of problems, in particular with shared state consistency.

The other approach which is far more popular in business-level modeling is to treat entities as actors, which can perform some tasks, but without interrupting it with calls to other contracts. Any calls to other contracts can only be called after the whole execution is performed. When "subcall" is finished, it will call the original contract back.

This solution may feel unnatural, and it requires different kinds of design solutions, but it turns out to work pretty well for smart contract execution. I will try to explain how to reason about it, and how it maps to contract structure step by step.

The Actor

The most important thing in the whole model is an Actor itself. So, what is this? The Actor is a single instantiation of a contract, which can perform several actions. When the actor finishes his job, he prepares a summary of it, which includes the list of things that have to be done, to complete the whole scheduled task.

An example of an actor is the Seller in the KFC restaurant. The first thing you do is order your BSmart, so you are requesting action from him. So, from the system user, you can think about this task as "sell and prepare my meal", but the action performed by the seller is just "Charge payment and create order". The first part of this operation is to create a bill and charge you for it, and then it requests the Sandwich and Fries to be prepared by other actors, probably chefs. Then when the chef is done with his part of the meal, he checks if all meals are ready. If so, it calls the last actor, the waiter, to deliver the food to you. At this point, you can receive your delivery, and the task is considered complete.

The above-described workflow is kind of simplified. In particular - in a typical restaurant, a waiter would observe the kitchen instead of being triggered by a chef, but in the Actor model, it is not possible. Here, entities of the system are passive and cannot observe the environment actively - they only react to messages from other system participants. Also in KFC, the seller would not schedule subtasks for particular chefs; instead, he would leave tasks to be taken by them, when they are free. It is not the case, because as before - chefs cannot actively listen to the environment. However, it would be possible to create a contract for being a chef's dispatcher which would collect all orders from sellers, and balance them across chefs for some reason.

The Action

Actors are the model entities, but to properly communicate with them, we need some kind of protocol. Every actor is capable of performing several actions. In my previous KFC example, the only action seller can do is "Charge payment and create order". However, it is not always the case - our chefs were proficient at performing both "Prepare fries" and "Prepare Sandwich" actions - and also many more.

So, when we want to do something in an actor system, we schedule some action to the actor being the closest to us, very often with some additional parameters (as we can pick if we want to exchange fries with salad).

However, naming the action after the exact thing which happened in the very contract would be misleading. Take a look at the KFC example once again. As I mentioned, the action performed by a seller is "Charge payment and create order". The problem is, that for the client who schedules this action, it doesn't matter what exactly is the responsibility of the actor himself - what the client is scheduling is "Prepare Meal" with some description of what exactly to prepare. So, we can say, that the action is the thing performed by the contract itself, plus all the sub-actions it schedules.

Multi-stage Actions

So as the whole idea makes some sense, there is the problem created by the actor model: what if I want to perform some action in my contract, but to completely finalize some steps, the contract has to make sure that some sub-action he scheduled are finished?

Imagine that in the previous KFC situation, there is no dedicated Waiter. Instead the Seller was serving you a meal when the Chefs finished their job.

This kind of pattern is so important and common that in CosmWasm, we developed a special way to handle it, which is dedicated Reply action.

So when Seller is scheduling actions for chefs, he assigns some number to this action (like order id) and passes it to chefs. He also remembers how many actions he scheduled for every order id. Now every time chef is finished with his action; he would call the special Reply action on Seller, in which he would pass back the order id. Then, Seller would decrease the number of actions left for this order, and if it reached zero, he would serve a meal.

Now you can say, that the Reply action is completely not needed, as Chefs could just schedule any arbitrary action on Seller, like Serve, why is there the special Reply for? The reason is abstraction and reusability. The Chefs task is to prepare a meal, and that is all. There is no reason for him to know why he is even preparing Fries - if it is part of the bigger task (like order for a client), or the seller is just hungry. It is possible that not only the seller is eligible to call the chef for food - possibly any restaurant employee can do that just for themselves. Therefore, we need a way to be able to react to an actor finishing his job in some universal way, to handle this situation properly in any context.

It is worth noting that the Reply can contain some additional data. The id assigned previously is the only required information in the Reply call, but the actor can pass some additional data - events emitted, which are mostly metadata (to be observed by non-blockchain applications mostly), and any arbitrary data it wants to pass.

State

Up until this point, we were considering actors as entities performing some job, like preparing the meal. If we are considering computer programs, such a job would be to show something on the screen, maybe print something. This is not the case with Smart Contracts. The only thing which can be affected by the Smart Contract is their internal state. So, the state is arbitrary data that is kept by the contract. Previously in the KFC example I mentioned, the Seller is keeping in mind how many actions he scheduled for chefs are not yet finished - this number is part of the Seller's state.

To give a more realistic example of a contract state, let's think about a more real-life Smart Contract than the restaurant. Let's imagine we want to create our currency - maybe we want to create some smart contracts-based market for some MMORPG game. So, we need some way to be able to at least transfer currency between players. We can do that, by creating the contract we would call MmoCurrency, which would support the Transfer action to transfer money to another player. Then what would be the state of such a contract? It would be just a table mapping player names to the amount of currency they own. The contract we just invited exists in CosmWasm examples, and it is called the cw20-base contract (it is a bit more complicated, but it is its core idea).

And now there is a question - how is this helpful to transfer currency if I cannot check how much of it do I own? It is a very good question, and the answer to that is simple - the whole state of every contract in our system is public. It is not universal for every Actor model, but it is how it works in CosmWasm, and it is kind of forced by the nature of blockchain. Everything happening in blockchain has to be public, and if some information should be hidden, it has to be stored indirectly.

There is one very important thing about the state in CosmWasm, and it is the state being transactional. Any updates to the state are not applied immediately, but only when the whole action succeeds. It is very important, as it guarantees that if something goes wrong in the contract, it is always left in some proper state. Let's consider our MmoCurrency case. Imagine, that in the Transfer action we first increase the receiver currency amount (by updating the state), and only then do we decrease the sender amount. However, before decreasing it, we need to check if a sender possesses enough funds to perform the transaction. In case we realize that we cannot do it, we don't need to do any rolling back by hand - we would just return a failure from the action execution, and the state would not be updated. So, when in the contract state is updated, it is just a local copy of this state being altered, but the partial changes would never be visible by other contracts.

Queries

There is one building block in the CosmWasm approach to the Actor model, which I haven't yet cover. As I said, the whole state of every contract is public and available for everyone to look at. The problem is that this way of looking at state is not very convenient - it requires users of contracts to know its internal structure, which kind of violates the SOLID rules (Liskov substitution principle in particular). If, for example a contract is updated and its state structure changes a bit, another contract looking at its state would just nevermore work. Also, it is often the case, that the contract state is kind of simplified, and information that is relevant to the observer would be calculated from the state.

This is where queries come into play. Queries are the type of messages to contract, which does not perform any actions, so do not update any state, but can return an answer immediately.

In our KFC comparison, the query would be if Seller goes to Chef to ask "Do we still have pickles available for our cheeseburgers"? It can be done while operating, and response can be used in it. It is possible because queries can never update their state, so they do not need to be handled in a transactional manner.

However, the existence of queries doesn't mean that we cannot look at the contract's state directly - the state is still public, and the technique of looking at them directly is called Raw Queries. For clarity, non-raw queries are sometimes denoted as Smart Queries.

Wrapping everything together - transactional call flow

So, we touched on many things here, and I know it may be kind of confusing. Because of that, I would like to go through some more complicated calls to the CosmWasm contract to visualize what the "transactional state" means.

Let's imagine two contracts:

  1. The MmoCurrency contract mentioned before, which can perform the Transfer action, allows transferring some amount of currency to some receiver.
  2. The WarriorNpc contract, which would have some amount of our currency, and he would be used by our MMO engine to pay the reward out for some quest player could perform. It would be triggered by Payout action, which can be called only by a specific client (which would be our game engine).

Now here is an interesting thing - this model forces us to make our MMO more realistic in terms of the economy that we traditionally see - it is because WarriorNpc has some amount of currency, and cannot create more out of anything. It is not always the case (the previously mentioned cw20 has a notion of Minting for this case), but for the sake of simplicity let's assume this is what we want.

To make the quest reasonable for longer, we would make a reward for it to be always between 1 mmo and 100 mmo, but it would be ideally 15% of what Warrior owns. This means that the quest reward decreases for every subsequent player, until Warrior would be broke, left with nothing, and will no longer be able to payout players.

So, what would the flow look like? The first game would send a Payout message to the WarriorNpc contract, with info on who should get the reward. Warrior would keep track of players who fulfilled the quest, to not pay out the same person twice - there would be a list of players in his state. First, he would check the list looking for players to pay out - if he is there, he will finish the transaction with an error.

However, in most cases the player would not be on the list - so then WarriorNpc would add him to the list. Now the Warrior would finish his part of the task, and schedule the Transfer action to be performed by MmoCurrency.

But there is the important thing - because Transfer action is actually part of the bigger Payout flow, it would not be executed on the original blockchain state, but on the local copy of it, to which the player's list is already applied to. So if the MmoCurrency would for any reason takes a look at WarriorNpc internal list, it would be already updated.

Now MmoCurrency is doing its job, updating the state of Warrior and player balance (note, that our Warrior is here just treated as another player!). When it finishes, two things may happen:

  1. There was an error - possibly Warrior is out of cash, and it can nevermore pay for the task. In such case, none of the changes - neither updating the list of players succeeding, nor balance changes are not applied to the original blockchain storage, so they are like they never happened. In the database world, it is denoted as rolling back the transaction.
  2. Operation succeed - all changes on the state are now applied to the blockchain, and any further observation of MmoCurrency or WarriorNpc by the external world would see updated data.

There is one problem - in this model, our list is not a list of players who fulfilled the quest (as we wanted it to be), but the list of players who paid out (as in transfer failure, the list is not updated). We can do better.

Different ways of handling responses

Note that we didn't mention a Reply operation at all. So why was it not called by MmoCurrency on WarriorNpc? The reason is that this operation is optional. When scheduling sub-actions on another contract we may choose when Reply how the result should be handled:

  1. Never call Reply, action fails if sub-message fails
  2. Call Reply on success
  3. Call Reply on failure
  4. Always call Reply

So, if we do not request Reply to be called by subsequent contract, it will not happen. In such a case if a sub-call fails, the whole transaction is rolled back - sub-message failure transitively causes the original message failure. It is probably a bit complicated for now, but I promise it would be simple if you would did some practice with that.

When handling the reply, it is important to remember, that although changes are not yet applied to the blockchain (the transaction still can be failed), the reply handler is already working on the copy of the state with all changes made by sub-message so far applied. In most cases, it would be a good thing, but it has a tricky consequence - if the contract is calling itself recursively, it is possible that subsequent call overwrote things set up in the original message. It rarely happens, but may need special treatment in some cases - for now I don't want to go deeply into details, but I want you to remember about what to expect after state in the actor's flow.

Now let's take a look at handling results with 2-4 options. It is actually interesting, that using 2, even if the transaction is performed by sub-call succeed, we may now take a look at the data it returned with Reply, and on its final state after it finished, and we can still decide, that act as a whole is a failure, in which case everything would be rolled back - even currency transfer performed by external contract.

In our case, an interesting option is 3. So, if the contract would call Reply on failure, we can decide to claim success, and commit a transaction on the state if the sub call failed. Why may it be relevant for us? Possibly because our internal list was supposed to keep the list of players succeeding with the quest, not paid out! So, if we have no more currency, we still want to update the list!

The most common way to use the replies (option 2 in particular) is to instantiate another contract, managed by the one called. The idea is that in those use cases, the creator contract wants to keep the address of the created contract in its state. To do so it has to create an Instantiate sub-message, and subscribe for its success response, which contains the address of the freshly created contract.

In the end, you can see that performing actions in CosmWasm is built with hierarchical state change transactions. The sub-transaction can be applied to the blockchain only if everything succeeds, but in case that sub-transaction failed, only its part may be rolled back, end other changes may be applied. It is very similar to how most database systems work.

Conclusion

Now you have seen the power of the actor model to avoid reentrancy, properly handle errors, and safely sandbox contracts. This helps us provide the solid security guarantees of the CosmWasm platform. Let’s get started playing around with real contracts in the wasmd blockchain.

Actors in blockchain

Previously we were talking about actors mostly in the abstraction of any blockchain-specific terms. However, before we would dive into the code, we need to establish some common language, and to do so we would look at contracts from the perspective of external users, instead of their implementation.

In this part, I would use the wasmd binary to communicate with the malaga testnet. To properly set it up, check the Quick start with wasmd.

Blockchain as a database

It is kind of starting from the end, but I would start with the state part of the actor model. Relating to traditional systems, there is one particular thing I like to compare blockchain with - it is a database.

Going back to the previous section we learned that the most important part of a contract is its state. Manipulating the state is the only way to persistently manifest work performed to the world. But What is the thing which purpose is to keep the state? It is a database!

So here is my (as a contract developer) point of view on contracts: it is a distributed database, with some magical mechanisms to make it democratic. Those "magical mechanisms" are crucial for BC's existence and they make they are reasons why even use blockchain, but they are not relevant from the contract creator's point of view - for us, everything that matters is the state.

But you can say: what about the financial part?! Isn't blockchain (wasmd in particular) the currency implementation? With all of those gas costs, sending funds seems very much like a money transfer, not database updates. And yes, you are kind of right, but I have a solution for that too. Just imagine, that for every native token (by "native tokens" we meant tokens handled directly by blockchain, in contradiction to for example cw20 tokens) there is a special database bucket (or table if you prefer) with mapping of address to how much of a token the address possesses. You can query this table (querying for token balance), but you cannot modify it directly. To modify it you just send a message to a special build-in bank contract. And everything is still a database.

But if blockchain is a database, then where are smart contracts stored? Obviously - in the database itself! So now imagine another special table - this one would contain a single table of code-ids mapped to blobs of wasm binaries. And again - to operate on this table, you use "special contract" which is not accessible from another contract, but you can use it via wasmd binary.

Now there is a question - why do I even care about BC being a DB? So the reason is that it makes reasoning about everything in blockchain very natural. Do you remember that every message in the actor model is transactional? It perfectly matches traditional database transactions (meaning: every message starts a new transaction)! Also, when we later talk about migrations, it would turn out, that migrations in CosmWasm are very much equivalents of schema migrations in traditional databases.

So, the thing to remember - blockchain is very similar to a database, having some specially reserved tables (like native tokens, code repository), with a special bucket created for every contract. A contract can look at every table in every bucket in the whole blockchain, but it can modify the only one he created.

Compile the contract

I will not go into the code for now, but to start with something we need compiled contract binary. The cw4-group contract from cw-plus is simple enough to work with, for now, so we will start with compiling it. Start with cloning the repository:

$ git clone git@github.com:CosmWasm/cw-plus.git

Then go to cw4-group contract and build it:

$ cd cw-plus/contracts/cw4-group
$ docker run --rm -v "$(pwd)":/code \
  --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \
  --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \
  cosmwasm/workspace-optimizer:0.12.6

Your final binary should be located in the cw-plus/artifacts folder (cw-plus being where you cloned your repository).

Contract code

When the contract binary is built, the first interaction with CosmWasm is uploading it to the blockchain (assuming you have your wasm binary in the working directory):

$ wasmd tx wasm store ./cw4-group.wasm --from wallet $TXFLAG -y -b block

As a result of such an operation you would get json output like this:

..
logs:
..
- events:
  ..
  - attributes:
    - key: code_id
      value: "1069"
    type: store_code

I ignored most of not fields as they are not relevant for now - what we care about is the event emitted by blockchain with information about code_id of stored contract - in my case the contract code was stored in blockchain under the id of 1069. I can now look at the code by querying for it:

$ wasmd query wasm code 1069 code.wasm

And now the important thing - the contract code is not an actor. So, what is a contract code? I think that the easiest way to think about that is a class or a type in programming. It defines some stuff about what can be done, but the class itself is in most cases not very useful unless we create an instance of a type, on which we can call class methods. So now let's move forward to instances of such contract classes.

Contract instance

Now we have a contract code, but what we want is an actual contract itself. To create it, we need to instantiate it. Relating to analogy to programming, instantiation is calling a constructor. To do that, I would send an instantiate message to my contract:

$ wasmd tx wasm instantiate 1069 '{"members": []}' --from wallet --label "Group 1" --no-admin $TXFLAG -y

What I do here is create a new contract and immediately call the Instantiate message on it. The structure of such a message is different for every contract code. In particular, the cw4-group Instantiate message contains two fields:

  • members field which is the list of initial group members optional admin
  • field which defines an address of who can add or remove a group member

In this case, I created an empty group with no admin - so which could never change! It may seem like a not very useful contract, but it serves us as a contract example.

As the result of instantiating, I got the result:

..
logs:
..
- events:
  ..
  - attributes:
    - key: _contract_address
      value: wasm1u0grxl65reu6spujnf20ngcpz3jvjfsp5rs7lkavud3rhppnyhmqqnkcx6
    - key: code_id
      value: "1069"
    type: instantiate

As you can see, we again look at logs[].events[] field, looking for interesting event and extracting information from it - it is the common case. I will talk about events and their attributes in the future but in general, it is a way to notify the world that something happened. Do you remember the KFC example? If a waiter is serving our dish, he would put a tray on the bar, and she would yell (or put on the screen) the order number - this would be announcing an event, so you know some summary of operation, so you can go and do something useful with it.

So, what use can we do with the contract? We obviously can call it! But first I want to tell you about addresses.

Addresses in CosmWasm

Address in CosmWasm is a way to refer to entities in the blockchain. There are two types of addresses: contract addresses, and non-contracts. The difference is that you can send messages to contract addresses, as there is some smart contract code associated with them, and non-contracts are just users of the system. In an actor model, contract addresses represent actors, and non-contracts represent clients of the system.

When operating with blockchain using wasmd, you also have an address - you got one when you added the key to wasmd:

# add wallets for testing
$ wasmd keys add wallet3
- name: wallet3
  type: local
  address: wasm1dk6sq0786m6ayg9kd0ylgugykxe0n6h0ts7d8t
  pubkey: '{"@type":"/cosmos.crypto.secp256k1.PubKey","key":"Ap5zuScYVRr5Clz7QLzu0CJNTg07+7GdAAh3uwgdig2X"}'
  mnemonic: ""

You can always check your address:

$ wasmd keys show wallet
- name: wallet
  type: local
  address: wasm1um59mldkdj8ayl5gknp9pnrdlw33v40sh5l4nx
  pubkey: '{"@type":"/cosmos.crypto.secp256k1.PubKey","key":"A5bBdhYS/4qouAfLUH9h9+ndRJKvK0co31w4lS4p5cTE"}'
  mnemonic: ""

Having an address is very important because it is a requirement for being able to call anything. When we send a message to a contract it always knows the address which sends this message so it can identify it - not to mention that this sender is an address that would play a gas cost.

Querying the contract

So, we have our contract, let's try to do something with it - query would be the easiest thing to do. Let's do it:

$ wasmd query wasm contract-state smart wasm1u0grxl65reu6spujnf20ngcpz3jvjfsp5rs7lkavud3rhppnyhmqqnkcx6 '{ "list_members": {} }'
data:
  members: []

The wasm... string is the contract address, and you have to substitute it with your contract address. { "list_members": {} } is query message we send to contract. Typically, CW smart contract queries are in the form of a single JSON object, with one field: the query name (list_members in our case). The value of this field is another object, being query parameters - if there are any. list_members query handles two parameters: limit, and start_after, which are both optional and which support result pagination. However, in our case of an empty group they don't matter.

The query result we got is in human-readable text form (if we want to get the JSON from - for example, to process it further with jq, just pass the -o json flag). As you can see response contains one field: members which is an empty array.

So, can we do anything more with this contract? Not much. But let's try to do something with a new one!

Executions to perform some actions

The problem with our previous contract is that for the cw4-group contract, the only one who can perform executions on it is an admin, but our contract doesn't have one. This is not true for every smart contract, but it is the nature of this one.

So, let's make a new group contract, but this time we would make ourselves an admin. First, check our wallet address:

$ wasmd keys show wallet

And instantiate a new group contract - this time with proper admin:

$ wasmd tx wasm instantiate 1069 '{"members": [], "admin": "wasm1um59mldkdj8ayl5gknp9pnrdlw33v40sh5l4nx"}' --from wallet --label "Group 1" --no-admin $TXFLAG -y
..
logs:
- events:
  ..
  - attributes:
    - key: _contract_address
      value: wasm1n5x8hmstlzdzy5jxd70273tuptr4zsclrwx0nsqv7qns5gm4vraqeam24u
    - key: code_id
      value: "1069"
    type: instantiate

You may ask, why do we pass some kind of --no-admin flag, if we just said, we want to set an admin to the contract? The answer is sad and confusing, but... it is a different admin. The admin we want to set is one checked by the contract itself and managed by him. The admin which is declined with --no-admin flag, is a wasmd-level admin, which can migrate the contract. You don't need to worry about the second one at least until you learn about contract migrations - until then you can always pass the --no-admin flag to the contract.

Now let's query our new contract for the member's list:

$ wasmd query wasm contract-state smart wasm1n5x8hmstlzdzy5jxd70273tuptr4zsclrwx0nsqv7qns5gm4vraqeam24u '{ "list_members": {} }'
data:
  members: []

Just like before - no members initially. Now check an admin:

$ wasmd query wasm contract-state smart wasm1n5x8hmstlzdzy5jxd70273tuptr4zsclrwx0nsqv7qns5gm4vraqeam24u '{ "admin": {} }'
data:
  admin: wasm1um59mldkdj8ayl5gknp9pnrdlw33v40sh5l4nx

So, there is an admin, it seems like the one we wanted to have there. So now we would add someone to the group - maybe ourselves?

wasmd tx wasm execute wasm1n5x8hmstlzdzy5jxd70273tuptr4zsclrwx0nsqv7qns5gm4vraqeam24u '{ "update_members": { "add": [{ "addr": "wasm1um59mldkdj8ayl5gkn
p9pnrdlw33v40sh5l4nx", "weight": 1 }], "remove": [] } }' --from wallet $TXFLAG -y

The message for modifying the members is update_members and it has two fields: members to remove, and members to add. Members to remove are just addresses. Members to add have a bit more complex structure: they are records with two fields: address and weight. Weight is not relevant for us now, it is just metadata stored with every group member - for us, it would always be 1.

Let's query the contract again to check if our message changed anything:

$ wasmd query wasm contract-state smart wasm1n5x8hmstlzdzy5jxd70273tuptr4zsclrwx0nsqv7qns5gm4vraqeam24u '{ "list_members": {} }'
data:
  members:
  - addr: wasm1um59mldkdj8ayl5gknp9pnrdlw33v40sh5l4nx
    weight: 1

As you can see, the contract updated its state. This is basically how it works - sending messages to contracts causes them to update the state, and the state can be queried at any time. For now, to keep things simple we were just interacting with the contract directly by wasmd, but as described before - contracts can communicate with each other. However, to investigate this we need to understand how to write contracts. Next time we will look at the contract structure and we will map it part by part to what we have learned until now.

Smart contract as an actor

In previous chapters, we talked about the actor model and how it is implemented in the blockchain. Now it is time to look closer into the typical contract structure to understand how different features of the actor model are mapped to it.

This will not be a step-by-step guide on contract creation, as it is a topic for the series itself. It would be going through contract elements roughly to visualize how to handle architecture in the actor model.

Also, we will work with the cw4-group contract from cw-plus, which was written in the old way so the contract structure will be different than the one we worked on in basics part. I decided to go with this approach as it visualizes the actor model concept very well.

The state

As before we would start with the state. Previously we were working with the cw4-group contract, so let's start by looking at its code. Go to cw-plus/contracts/cw4-group/src. The folder structure should look like this:

  src
├──  contract.rs
├──  error.rs
├──  helpers.rs
├──  lib.rs
├──  msg.rs
└──  state.rs

As you may already figure out, we want to check the state.rs first.

The most important thing here is a couple of constants: ADMIN, HOOKS, TOTAL, and MEMBERS. Every one of such constants represents a single portion of the contract state - as tables in databases. The types of those constants represent what kind of table this is. The most basic ones are Item<T>, which keeps zero or one element of a given type, and Map<K, T> which is a key-value map.

You can see Item is used to keep an admin and some other data: HOOKS, and TOTAL. HOOKS is used by the cw4-group to allow subscription to any changes to a group - a contract can be added as a hook, so when the group changes, a message is sent to it. The TOTAL is just a sum of all members' weights.

The MEMBERS in the group contract is the SnapshotMap - as you can imagine, it is a Map, with some steroids - this particular one, gives us access to the state of the map at some point in history, accessing it by the blockchain height. height is the count of blocks created since the beggining of blockchain, and it is the most atomic time representation in smart contracts. There is a way to access the clock time in them, but everything happening in a single block is considered happening in the same moment.

Other types of storage objects not used in group contracts are:

  • IndexedMap - another map type, that allows accessing values by a variety of keys
  • IndexedSnapshotMap - IndexedMap and SnapshotMap married

What is very important - every state type in the contract is accessed using some name. All of those types are not containers, just accessors to the state. Do you remember that I told you before that blockchain is our database? And that is correct! All those types are just ORM to this database - when we use them to get actual data from it, we pass a special State object to them, so they can retrieve items from it.

You may ask - why all that data for a contract are not auto-fetched by whatever is running it. That is a good question. The reason is that we want contracts to be lazy with fetching. Copying data is a very expensive operation, and for everything happening on it, someone has to pay - it is realized by gas cost. I told you before, that as a contract developer you don't need to worry about gas at all, but it was only partially true. You don't need to know exactly how gas is calculated, but by lowering your gas cost, you would may execution of your contracts cheaper which is typically a good thing. One good practice to achieve that is to avoid fetching data you will not use in a particular call.

Messages

In a blockchain, contracts communicate with each other by some JSON messages. They are defined in most contracts in the msg.rs file. Take a look at it.

There are three types on it, let's go through them one by one. The first one is an InstantiateMsg. This is the one, that is sent on contract instantiation. It typically contains some data which is needed to properly initialize it. In most cases, it is just a simple structure.

Then there are two enums: ExecuteMsg, and QueryMsg. They are enums because every single variant of them represents a different message which can be sent. For example, the ExecuteMsg::UpdateAdmin corresponds to the update_admin message we were sending previously.

Note, that all the messages are attributed with #[derive(Serialize, Deserialize)], and #[serde(rename_all="snake_case")]. Those attributes come from the serde crate, and they help us with deserialization of them (and serialization in case of sending them to other contracts). The second one is not required, but it allows us to keep a camel-case style in our Rust code, and yet still have JSONs encoded with a snake-case style more typical to this format.

I encourage you to take a closer look at the serde documentation, like everything there, can be used with the messages.

One important thing to notice - empty variants of those enums, tend to use the empty brackets, like Admin {} instead of more Rusty Admin. It is on purpose, to make JSONs cleaner, and it is related to how serde serializes enum.

Also worth noting is that those message types are not set in stone, they can be anything. This is just a convention, but sometimes you would see things like ExecuteCw4Msg, or similar. Just keep in mind, to keep your message name obvious in terms of their purpose - sticking to ExecuteMsg/QueryMsg is generally a good idea.

Entry points

So now, when we have our contract message, we need a way to handle them. They are sent to our contract via entry points. There are three entry points in the cw4-group contract:


#![allow(unused)]
fn main() {
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
    deps: DepsMut,
    env: Env,
    _info: MessageInfo,
    msg: InstantiateMsg,
) -> Result<Response, ContractError> {
    // ...
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    msg: ExecuteMsg,
) -> Result<Response, ContractError> {
    // ..
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
    // ..
}
}

Those functions are called by the CosmWasm virtual machine when a message is to be handled by contract. You can think about them as the main function of normal programs, except they have a signature that better describes the blockchain itself.

What is very important is that the names of those entry points (similarly to the main function) are fixed - it is relevant, so the virtual machine knows exactly what to call.

So, let's start with the first line. Every entry point is attributed with #[cfg_attr(not(feature = "library"), entry_point)]. It may look a bit scary, but it is just a conditional equivalent of #[entry_point] - the attribute would be there if and only if the "library" feature is not set. We do this to be able to use our contracts as dependencies for other contracts - the final binary can contain only one copy of each entry point, so we make sure, that only the top-level one is compiled without this feature.

The entry_point attribute is a macro that generates some boilerplate. As the binary is run by WASM virtual machine, it doesn't know much about Rust types - the actual entry point signatures are very inconvenient to use. To overcome this issue, there is a macro created, which generates entry points for us, and those entry points are just calling our functions.

Now take a look at functions arguments. Every single entry point takes as the last argument a message which triggered the execution of it (except for reply - I will explain it later). In addition to that, there are additional arguments provided by blockchain:

  • Deps or DepsMut object is the gateway to the world outside the smart contract context. It allows accessing the contract state, as well as querying other contracts, and also delivers an Api object with a couple of useful utility functions. The difference is that DepsMut allows updating state, while Deps allows only to look at it.
  • Env object delivers information about the blockchain state at the moment of execution - its height, the timestamp of execution and information about the executing contract itself.
  • MessageInfo object is information about the contract call - it contains the address which sends the message, and the funds sent with the message.

Keep in mind, that the signatures of those functions are fixed (except the messages type), so you cannot interchange Deps with DepsMut to update the contract state in the query call.

The last portion of entry points is the return type. Every entry point returns a Result type, with any error which can be turned into a string - in case of contract failure, the returned error is just logged. In most cases, the error type is defined for a contract itself, typically using a thiserror crate. Thiserror is not required here, but is strongly recommended - using it makes the error definition very straightforward, and also improves the testability of the contract.

The important thing is the Ok part of Result. Let's start with the query because this one is the simplest. The query always returns the Binary object on the Ok case, which would contain just serialized response. The common way to create it is just calling a to_binary method on an object implementing serde::Serialize, and they are typically defined in msg.rs next to message types.

Slightly more complex is the return type returned by any other entry point - the cosmwasm_std::Response type. This one keep everything needed to complete contract execution. There are three chunks of information in that.

The first one is an events field. It contains all events, which would be emitted to the blockchain as a result of the execution. Events have a really simple structure: they have a type, which is just a string, and a list of attributes which are just string-string key-value pairs.

You can notice that there is another attributes field on the Response. This is just for convenience - most executions would return only a single event, and to make it a bit easier to operate one, there is a set of attributes directly on response. All of them would be converted to a single wasm event which would be emitted. Because of that, I consider events and attributes to be the same chunk of data.

Then we have the messages field, of SubMsg type. This one is the clue of cross-contact communication. Those messages would be sent to the contracts after processing. What is important - the whole execution is not finished, unless the processing of all sub-messages scheduled by the contract finishes. So, if the group contract sends some messages as a result of update_members execution, the execution would be considered done only if all the messages sent by it would also be handled (even if they failed).

So, when all the sub-messages sent by contract are processed, then all the attributes generated by all sub-calls and top-level calls are collected and reported to the blockchain. But there is one additional piece of information - the data. So, this is another Binary field, just like the result of a query call, and just like it, it typically contains serialized JSON. Every contract call can return some additional information in any format. You may ask - in this case, why do we even bother returning attributes? It is because of a completely different way of emitting events and data. Any attributes emitted by the contract would be visible on blockchain eventually (unless the whole message handling fails). So, if your contract emitted some event as a result of being sub-call of some bigger use case, the event would always be there visible to everyone. This is not true for data. Every contract call would return only a single data chunk, and it has to decide if it would just forward the data field of one of the sub-calls, or maybe it would construct something by itself. I would explain it in a bit more detail in a while.

Sending submessages

I don't want to go into details of the Response API, as it can be read directly from documentation, but I want to take a bit closer look at the part about sending messages.

The first function to use here is add_message, which takes as an argument the CosmosMsg (or rather anything convertible to it). A message added to response this way would be sent and processed, and its execution would not affect the result of the contract at all.

The other function to use is add_submessage, taking a SubMsg argument. It doesn't differ much from add_message - SubMsg just wraps the CosmosMsg, adding some info to it: the id field, and reply_on. There is also a gas_limit thing, but it is not so important - it just causes sub-message processing to fail early if the gas threshold is reached.

The simple thing is reply_on - it describes if the reply message should be sent on processing success, on failure, or both.

The id field is an equivalent of the order id in our KFC example from the very beginning. If you send multiple different sub-messages, it would be impossible to distinguish them without that field. It would not even be possible to figure out what type of original message reply handling is! This is why the id field is there - sending a sub-message you can set it to any value, and then on the reply, you can figure out what is happening based on this field.

An important note here - you don't need to worry about some sophisticated way of generating ids. Remember, that the whole processing is atomic, and only one execution can be in progress at once. In most cases, your contract sends a fixed number of sub-messages on very concrete executions. Because of that, you can hardcode most of those ids while sending (preferably using some constant).

To easily create submessages, instead of setting all the fields separately, you would typically use helper constructors: SubMsg::reply_on_success, SubMsg::reply_on_error and SubMsg::reply_always.

CosmosMsg

If you took a look at the CosmosMsg type, you could be very surprised - there are so many variants of them, and it is not obvious how they relate to communication with other contracts.

The message you are looking for is the WasmMsg (CosmosMsg::Wasm variant). This one is very much similar to what we already know - it has a couple of variants of operation to be performed by contracts: Execute, but also Instantiate (so we can create new contracts in contract executions), and also Migrate, UpdateAdmin, and ClearAdmin - those are used to manage migrations (will tell a bit about them at the end of this chapter).

Another interesting message is the BankMsg (CosmosMsg::Bank). This one allows a contract to transfer native tokens to other contracts (or burn them - equivalent to transferring them to some black whole contract). I like to think about it as sending a message to a very special contract responsible for handling native tokens - this is not a true contract, as it is handled by the blockchain itself, but at least to me it simplifies things.

Other variants of CosmosMsg are not very interesting for now. The Custom one is there to allow other CosmWasm-based blockchains to add some blockchain-handled variant of the message. This is a reason why most message-related types in CosmWasm are generic over some T - this is just a blockchain-specific type of message. We will never use it in the wasmd. All other messages are related to advanced CosmWasm features, and I will not describe them here.

Reply handling

So now that we know how to send a submessage, it is time to talk about handling the reply. When sub-message processing is finished, and it is requested to reply, the contract is called with an entry point:


#![allow(unused)]
fn main() {
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result<Response, ContractError> {
    // ...
}
}

The DepsMut, and Env arguments are already familiar, but there is a new one, substituting the typical message argument: the cosmwasm_std::Reply.

This is a type representing the execution status of the sub-message. It is slightly processed cosmwasm_std::Response. The first important thing it contains is an id - the same, which you set sending sub-message, so now you can identify your response. The other one is the ContractResult, which is very similar to the Rust Result<T, String> type, except it is there for serialization purposes. You can easily convert it into a Result with an into_result function.

In the error case of ContracResult, there is a string - as I mentioned before, errors are converted to strings right after execution. The Ok case contains SubMsgExecutionResponse with two fields: events emitted by sub-call, and the data field embedded on response.

As said before, you never need to worry about forwarding events - CosmWasm would do it anyway. The data however, is another story. As mentioned before, every call would return only a single data object. In the case of sending sub-messages and not capturing a reply, it would always be whatever is returned by the top-level message. But it is not the case when reply is called. If a a reply is called, then it is a function deciding about the final data. It can decide to either forward the data from the sub-message (by returning None) or to overwrite it. It cannot choose, to return data from the original execution processing - if the contract sends sub-messages waiting for replies, it is supposed to not return any data, unless replies are called.

But what happens if multiple sub-messages are sent? What would the final data contain? The rule is - the last non-None. All sub-messages are always called in the order of adding them to the Response. As the order is deterministic and well defined, it is always easy to predict which reply would be used.

Migrations

I mentioned migrations earlier when describing the WasmMsg. So, migration is another action possible to be performed by contracts, which is kind of similar to instantiate. In software engineering, it is a common thing to release an updated version of applications. It is also a case in the blockchain - SmartContract can be updated with some new features. In such cases, a new code is uploaded, and the contract is migrated - so it knows that from this point, its messages are handled by another, updated contract code.

However, it may be that the contract state used by the older version of the contract differs from the new one. It is not a problem if some info was added (for example some additional map - it would be just empty right after migration). But the problem is, when the state changes, for example, the field is renamed. In such a case, every contract execution would fail because of (de)serialization problems. Or even more subtle cases, like adding a map, but one which should be synchronized with the whole contract state, not empty.

This is the purpose of the migration entry point. It looks like this:


#![allow(unused)]
fn main() {
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn migrate(deps: DepsMut, env: Env, msg: MigrateMsg) -> Result<Response<T>, ContracError> {
    // ..
}
}

MigrateMsg is the type defined by the contract in msg.rs. The migrate entry point would be called at the moment of performing the migration, and it is responsible for making sure the state is correct after the migration. It is very similar to schema migrations in traditional database applications. And it is also kind of difficult, because of version management involved - you can never assume, that you are migrating a contract from the previous version - it can be migrated from any version, released anytime - even later than that version we are migrating to!

It is worth bringing back one issue from the past - the contract admin. Do you remember the --no-admin flag we set previously on every contract instantiation? It made our contract unmigrateable. Migrations can be performed only by contract admin. To be able to use it, you should pass --admin address flag instead, with the address being the address that would be able to perform migrations.

Sudo

Sudo is the last basic entry point in CosmWasm, and it is the one we would never use in wasmd. It is equivalent to CosmosMsg::Custom, but instead of being a special blockchain-specific message to be sent and handled by a blockchain itself, it is now a special blockchain-specific message sent by the blockchain to contract in some conditions. There are many uses for those, but I will not cover them, because would not be related to CosmWasm itself. The signature of sudo looks like this:


#![allow(unused)]
fn main() {
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn sudo(deps: DepsMut, env: Env, msg: SudoMsg) -> Result<Response, ContractError> {
    // ..
}
}

The important difference is that because sudo messages are blockchain specific, the SudoMsg type is typically defined by some blockchain helper crate, not the contract itself.

Legal Information

Infomation according to § 5 TMG

Provider

Confio GmbH
7th Floor
Potsdamer Platz 1
10785 Berlin

Managing Director

Simon Warta

Contact

hello@confio.gmbh

Commercial Register

HRB 221575, Amtsgericht Charlottenburg

VAT number

DE339802279

Responsible for the content

Simon Warta
c/o Confio GmbH
7th Floor
Potsdamer Platz 1
10785 Berlin