Introduction

This book is a guide for creating CosmWasm smart contracts. 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.

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.

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 check_contract 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.0

Verifying the installation

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

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

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

cw-plus is a great place to find example contracts - look for them in contracts directory. The repository is maintained by CosmWasm creators, so contracts in there should follow good practices.

To verify the check_contract 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

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:

cw-plus $ cosmwasm-check ../../target/wasm32-unknown-unknown/release/cw1_whitelist.wasm
Supported features: {"stargate", "iterator", "staking"}
contract checks passed.

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.

Basics

In this chapter, we will go through creating basic smart contracts step by step. I will try to 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 ./empty-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.0.0-beta8", 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, there is 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 and a couple of 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 which 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 are able to 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, Binary, Deps, 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 start with importing couple of types just for more consistent usage. Then we define our entry point. The instantiate takes four arguments:

  • deps: DepsMut is a utility type for communicating with the outer world - it allows querying and updating the contract state, querying other contracts state, and gives 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 with this guide, 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.

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

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.

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"
wasm-debug = "build --target wasm32-unknown-unknown"

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 check_contract on it:

$ cargo wasm
...
$ check_contract ./target/wasm32-unknown-unknown/release/contract.wasm
Supported features: {"iterator", "staking", "stargate"}
contract checks passed.

Creating a query

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

First, we need to add serde crate to our dependencies. It would help us with the serialization and deserialization of query messages. Update the Cargo.toml:

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

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

[dependencies]
cosmwasm-std = { version = "1.0.0-beta8", features = ["staking"] }
serde = { version = "1.0.103", default-features = false, features = ["derive"] }

[dev-dependencies]
cw-multi-test = "0.13.4"

Now go to your src/lib.rs file, and add a new query entry point:

use cosmwasm_std::{
    entry_point, to_binary, Binary, Deps, DepsMut, Empty, Env, MessageInfo,
    Response, StdResult,
};
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
struct QueryResp {
    message: String,
}

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

#[entry_point]
pub fn query(_deps: Deps, _env: Env, _msg: Empty) -> StdResult<Binary> {
    let resp = QueryResp {
        message: "Hello World".to_owned(),
    };

    to_binary(&resp)
}

Note that I omitted the previously created instantiate endpoint for simplicity - not to overload you with code, I will always only show lines that changed in the code.

We first need a structure we will return from our query. We always want to return something which is serializable. We are just deriving the Serialize and Deserialize traits from serde crate.

Then we need to implement our entry point. It is very similar to the instantiate one. The first significant difference is a type of deps argument. For instantiate, it was a DepMut, but here we went with a Deps object. That is because the query can never alter the smart contract's internal state. 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 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 supposed just purely to return some transformed contract state. It can be calculated based on some chain metadata (so the state can "automatically" change after some time), but not on message info.

Note that our entry point still has the same Empty type for its msg argument - it means that the query message we would send to the contract is still an empty JSON: {}

The last thing that changed is the return type. Instead of returning the Response type on the success case, we return an arbitrary serializable object. This is 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.

Now take a look at the implementation. Nothing complicated happens there - we create an object we want to return and encode it to the Binary type using the to_binary function.

Improving the message

We have a query, but there is a problem with the query message. It is always an empty JSON. It is terrible - if we would like to add another query in the future, it would be difficult to have any reasonable distinction between query variants.

In practice, we address this by using a non-empty query message type. Improve our contract:

use cosmwasm_std::{
    entry_point, to_binary, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdResult,
};
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
struct QueryResp {
    message: String,
}

#[derive(Serialize, Deserialize)]
pub enum QueryMsg {
    Greet {},
}

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

#[entry_point]
pub fn query(_deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
    use QueryMsg::*;

    match msg {
        Greet {} => {
            let resp = QueryResp {
                message: "Hello World".to_owned(),
            };

            to_binary(&resp)
        }
    }
}

Now we introduced a proper message type for the query message. It is an enum, and by default, it would serialize to a JSON with a single field - the name of the field will be an enum variant (in our case - always "greet" - at least for now), and the value of this field would be an object assigned to this enum variant.

Note that our enum has no type assigned to the only Greet 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 { "greet": {} }, the JSON representation of this variant would be "greet". This behavior brings inconsistency in the message schema. It is, generally, a good habit to always add the {} to serde serializable empty enum variants - for better JSON representation.

But now, we can still improve the code further. Right now, the query function has two responsibilities. The first is obvious - handling the query itself. It was the first assumption, and it is still there. But there is a new thing happening there - the query message dispatching. It may not be obvious, as there is a single variant, but the query function is an excellent way to become a massive unreadable match statement. To make the code more SOLID, we will refactor it and take out handling the greet message to a separate function.

use cosmwasm_std::{
    entry_point, to_binary, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdResult,
};
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
pub struct GreetResp {
    message: String,
}

#[derive(Serialize, Deserialize)]
pub enum QueryMsg {
    Greet {},
}

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

#[entry_point]
pub fn query(_deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
    use QueryMsg::*;

    match msg {
        Greet {} => to_binary(&query::greet()?),
    }
}

mod query {
    use super::*;

    pub fn greet() -> StdResult<GreetResp> {
        let resp = GreetResp {
            message: "Hello World".to_owned(),
        };

        Ok(resp)
    }
}

Now it looks much better. Note there are a couple of additional improvements. I renamed the query-response type GreetResp as I may have different responses for different queries. I want the name to relate only to the variant, not the whole message.

Next is enclosing my new function in the module query. It makes it easier to avoid name collisions - I can have the same variant for queries and execution messages in the future, and their handlers would lie in separate namespaces.

A questionable decision may be returning StdResult instead of GreetResp from greet function, as it would never return an error. It is a matter of style, but I prefer consistency over the message handler, and the majority of them would have failure cases - e.g. when reading the state.

Also, you might pass deps and env arguments to all your query handlers for consistency. I'm not too fond of this, as it introduces unnecessary boilerplate which doesn't read well, but I also agree with the consistency argument - I leave it to your judgment.

Structuring the contract

You can see that our contract is becoming a bit bigger now. About 50 lines are maybe not so much, but there are many different entities in a single file, and I think we can do better. I can already distinguish three different types of entities in the code: entry points, messages, and handlers. In most contracts, we would divide them across three files. Start with extracting all the messages to src/msg.rs:

use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
pub struct GreetResp {
    pub message: String,
}

#[derive(Serialize, Deserialize)]
pub enum QueryMsg {
    Greet {},
}

You probably noticed that I made my GreetResp fields public. It is because they have to be now accessed from a different module. Now move forward to the src/contract.rs file:

use crate::msg::{GreetResp, QueryMsg};
use cosmwasm_std::{
    to_binary, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdResult,
};

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

pub fn query(_deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
    use QueryMsg::*;

    match msg {
        Greet {} => to_binary(&query::greet()?),
    }
}

mod query {
    use super::*;

    pub fn greet() -> StdResult<GreetResp> {
        let resp = GreetResp {
            message: "Hello World".to_owned(),
        };

        Ok(resp)
    }
}

I moved most of the logic here, so my src/lib.rs is just a very thin library entry with nothing else but module definitions and entry points definition. I removed the #[entry_point] attribute from my query function in src/contract.rs. I will have a function with this attribute. Still, I wanted to split functions' responsibility further - not the contract::query function is the top-level query handler responsible for dispatching the query message. The query function on crate-level is only an entry point. It is a subtle distinction, but it will make sense in the future when we would like not to generate the entry points but to keep the dispatching functions. I introduced the split now to show you the typical contract structure.

Now the last part, the src/lib.rs file:

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

mod contract;
mod msg;

#[entry_point]
pub fn instantiate(deps: DepsMut, env: Env, info: MessageInfo, msg: Empty)
  -> StdResult<Response>
{
    contract::instantiate(deps, env, info, msg)
}

#[entry_point]
pub fn query(deps: Deps, env: Env, msg: msg::QueryMsg)
  -> StdResult<Binary>
{
    contract::query(deps, env, msg)
}

Straightforward top-level module. Definition of submodules and entry points, nothing more.

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 knowledge besides Rust. Go to the src/contract.rs and add a test in its module:

use crate::msg::{GreetResp, QueryMsg};
use cosmwasm_std::{
    to_binary, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdResult,
};

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

pub fn query(_deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
    use QueryMsg::*;

    match msg {
        Greet {} => to_binary(&query::greet()?),
    }
}

mod query {
    use super::*;

    pub fn greet() -> StdResult<GreetResp> {
        let resp = GreetResp {
            message: "Hello World".to_owned(),
        };

        Ok(resp)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn greet_query() {
        let resp = query::greet().unwrap();
        assert_eq!(
            resp,
            GreetResp {
                message: "Hello World".to_owned()
            }
        );
    }
}

If you ever wrote a unit test in Rust, nothing should surprise you here. Just a simple test-only module contains local function unit tests. The problem is - this test doesn't build yet. We need to tweak our message types a bit. Update the src/msg.rs:

use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct GreetResp {
    pub message: String,
}

#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub enum QueryMsg {
    Greet {},
}

I added three new derives to both message types. PartialEq is required to allow comparing types for equality - so we can check if they are equal. The Debug is a trait generating debug-printing utilities. It is used by assert_eq! to display information about mismatch if an assertion fails. Note that because we are not testing the QueryMsg in any way, the additional trait derives are optional. Still, it is a good practice to make all messages both PartialEq and Debug for testability and consistency. The last one, Clone is not needed for now yet, but it is also good practice to allow messages to be cloned around. We will also require that later, so I added it already not to go back and forth.

Now we are ready to run our test:

$ cargo test

...
running 1 test
test contract::tests::greet_query ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Yay! Test passed!

Contract as a black box

Now let's go a step further. The Rust testing utility is a friendly tool for building even higher-level tests. We are currently testing smart contract internals, but if you think about how your smart contract is visible from the outside world. It is a single entity that is triggered by some input messages. We can create tests that treat the whole contract as a black box by testing it via our query function. Let's update our test:

use crate::msg::{GreetResp, QueryMsg};
use cosmwasm_std::{
    to_binary, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdResult,
};

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

pub fn query(_deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
    use QueryMsg::*;

    match msg {
        Greet {} => to_binary(&query::greet()?),
    }
}

mod query {
    use super::*;

    pub fn greet() -> StdResult<GreetResp> {
        let resp = GreetResp {
            message: "Hello World".to_owned(),
        };

        Ok(resp)
    }
}

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

    use super::*;

    #[test]
    fn greet_query() {
        let resp = query(
            mock_dependencies().as_ref(),
            mock_env(),
            QueryMsg::Greet {}
        ).unwrap();
        let resp: GreetResp = from_binary(&resp).unwrap();

        assert_eq!(
            resp,
            GreetResp {
                message: "Hello World".to_owned()
            }
        );
    }
}

We needed to produce two entities for the query functions: the deps and env instances. Hopefully, cosmwasm-std provides utilities for testing those - mock_dependencies and mock_env functions.

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

We can rerun the test, and it should still pass. But when we think about that test reflecting the actual use case, it is inaccurate. The contract is queried, but it was never instantiated! In software engineering, it is equivalent to calling a getter without constructing an object - taking it out of nowhere. It is a lousy testing approach. We can do better:


use crate::msg::{GreetResp, QueryMsg};
use cosmwasm_std::{
    to_binary, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdResult,
};

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

pub fn query(_deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
    use QueryMsg::*;

    match msg {
        Greet {} => to_binary(&query::greet()?),
    }
}

mod query {
    use super::*;

    pub fn greet() -> StdResult<GreetResp> {
        let resp = GreetResp {
            message: "Hello World".to_owned(),
        };

        Ok(resp)
    }
}

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

    use super::*;

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

        instantiate(
            deps.as_mut(),
            env.clone(),
            mock_info("sender", &[]),
            Empty {},
        )
        .unwrap();

        let resp = query(deps.as_ref(), env, QueryMsg::Greet {}).unwrap();
        let resp: GreetResp = from_binary(&resp).unwrap();
        assert_eq!(
            resp,
            GreetResp {
                message: "Hello World".to_owned()
            }
        );
    }
}

A couple of new things here. First, I extracted the deps and env variables to their variables and passed them to calls. The idea is that those variables 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 on 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 funds 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 a real-case scenario. I see just one problem. I say that the contract is a single black box. But here, nothing connects the instantiate call to the corresponding query. It seems that we assume there is some global contract. But it seems that if we would like to have two contracts instantiated differently in a single test case, it would become a mess. If only there would be some tool to 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.

First, we need to add a multitest to our Cargo.toml.

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

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

[dependencies]
cosmwasm-std = { version = "1.0.0-beta8", features = ["staking"] }
serde = { version = "1.0.103", default-features = false, features = ["derive"] }

[dev-dependencies]
cw-multi-test = "0.13.4"

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.

When we have the dependency ready, update our test to use the framework:

use crate::msg::{GreetResp, QueryMsg};
use cosmwasm_std::{
    to_binary, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdResult,
};

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

pub fn query(_deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
    use QueryMsg::*;

    match msg {
        Greet {} => to_binary(&query::greet()?),
    }
}

#[allow(dead_code)]
pub fn execute(
    _deps: DepsMut,
    _env: Env,
    _info: MessageInfo,
    _msg: Empty
) -> StdResult<Response> {
    unimplemented!()
}

mod query {
    use super::*;

    pub fn greet() -> StdResult<GreetResp> {
        let resp = GreetResp {
            message: "Hello World".to_owned(),
        };

        Ok(resp)
    }
}

#[cfg(test)]
mod tests {
    use cosmwasm_std::Addr;
    use cw_multi_test::{App, ContractWrapper, Executor};

    use super::*;

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

        let code = ContractWrapper::new(execute, instantiate, query);
        let code_id = app.store_code(Box::new(code));

        let addr = app
            .instantiate_contract(
                code_id,
                Addr::unchecked("owner"),
                &Empty {},
                &[],
                "Contract",
                None,
            )
            .unwrap();

        let resp: GreetResp = app
            .wrap()
            .query_wasm_smart(addr, &QueryMsg::Greet {})
            .unwrap();

        assert_eq!(
            resp,
            GreetResp {
                message: "Hello World".to_owned()
            }
        );
    }
}

You probably notice that I added the function for an execute entry point. I didn't add the entry point itself or the function's implementation, but for the multitest purposes contract has to contain at least instantiate, query, and execute handlers. I attributed the function as #[allow(dead_code)], so, cargo will not complain about it not being used anywhere. Enabling it for tests only with #[cfg(test)] would also be a way.

Then at the beginning of the test, I created the App object. It is a core multitest entity representing the virtual blockchain on which we will run our contracts. As you can see, we can call functions on it just like we would interact with blockchain using wasmd!

Right after creating app, I prepared the representation of the code, which would be "uploaded" to the blockchain. As multitests are just native Rust tests, they do not involve any Wasm binaries, but this name matches well what happens in a real-life scenario. We store this object in the blockchain with the store_code function, and as a result, we are getting the code id - we would need it to instantiate a contract.

Instantiation is the next step. In a single instantiate_contract call, we provide everything we would provide via wasmd - the contract code id, the address which performs instantiation,

the message triggering it, and any funds sent with the message (again - empty for now). We are adding the contract label and its admin for migrations - None, as we don't need it yet.

And after the contract is online, we can query it. The wrap function is an accessor for querying Api (queries are handled a bit differently than other calls), and the query_wasm_smart queries are given a contract with the message. Also, we don't need to care about query results as Binary - multitest assumes that we would like to deserialize them to some response type, so it takes advantage of Rust type elision to provide us with a nice Api.

Now it's time to rerun the test. It should still pass, but now we nicely abstracted the testing contract as a whole, not some internal functions. The next thing we should probably cover is making the contract more interesting by adding some state.

Contract state

The contract we are working on already has some behavior that can answer a query. Unfortunately, it is very predictable with its answers, and it has nothing to alter them. In this chapter, I introduce the notion of state, which would allow us to bring true life to a smart contract.

The state would still be static for now - it would be initialized on contract instantiation. The state would 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.0.0-beta8", features = ["staking"] }
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
cw-storage-plus = "0.13.4"

[dev-dependencies]
cw-multi-test = "0.13.4"

Now create a new file where you will keep a state for the contract - we typically call it src/state.rs:

use cosmwasm_std::Addr;
use cw_storage_plus::Item;

pub const ADMINS: Item<Vec<Addr>> = Item::new("admins");

The new thing we have here is the ADMINS constant of type Item<Vec<Addr>>. You could ask an excellent question here - how is the state constant? How do I modify it if it is a constant value?

The answer is tricky - this constant is not keeping the state itself. The state is stored in the blockchain and is accessed 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 in any way), 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 items keys intelligently. For now, we just used the simplest storage entity - and Item<_>, which holds a single optional value of a given type - Vec<Addr> in this case. And what would be a key to this item in the storage? It doesn't matter to us - it would be figured out to be unique, based on a unique string passed to the new function.

Before we would go into initializing our state, we need some better instantiate message. Go to src/msg.rs and create one:

use cosmwasm_std::Addr;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct InstantiateMsg {
    pub admins: Vec<String>,
}

#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct GreetResp {
    pub message: String,
}

#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub enum QueryMsg {
    Greet {},
}

Now go forward to instantiate the entry point, and initialize our state to whatever we got in the instantiation message:

use crate::msg::{GreetResp, InstantiateMsg, QueryMsg};
use crate::state::ADMINS;
use cosmwasm_std::{
    to_binary, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdResult,
};

pub fn instantiate(
    deps: DepsMut,
    _env: Env,
    _info: MessageInfo,
    msg: InstantiateMsg,
) -> StdResult<Response> {
    let admins: StdResult<Vec<_>> = msg
        .admins
        .into_iter()
        .map(|addr| deps.api.addr_validate(&addr))
        .collect();
    ADMINS.save(deps.storage, &admins?)?;

    Ok(Response::new())
}

pub fn query(_deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
    use QueryMsg::*;

    match msg {
        Greet {} => to_binary(&query::greet()?),
    }
}

#[allow(dead_code)]
pub fn execute(_deps: DepsMut, _env: Env, _info: MessageInfo, _msg: Empty) -> StdResult<Response> {
    unimplemented!()
}

mod query {
    use super::*;

    pub fn greet() -> StdResult<GreetResp> {
        let resp = GreetResp {
            message: "Hello World".to_owned(),
        };

        Ok(resp)
    }
}

#[cfg(test)]
mod tests {
    use cosmwasm_std::Addr;
    use cw_multi_test::{App, ContractWrapper, Executor};

    use super::*;

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

        let code = ContractWrapper::new(execute, instantiate, query);
        let code_id = app.store_code(Box::new(code));

        let addr = app
            .instantiate_contract(
                code_id,
                Addr::unchecked("owner"),
                &Empty {},
                &[],
                "Contract",
                None,
            )
            .unwrap();

        let resp: GreetResp = app
            .wrap()
            .query_wasm_smart(addr, &QueryMsg::Greet {})
            .unwrap();

        assert_eq!(
            resp,
            GreetResp {
                message: "Hello World".to_owned()
            }
        );
    }
}

We also need to update the message type on entry point in src/lib.rs:

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

mod contract;
mod msg;
mod state;

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

#[entry_point]
pub fn query(deps: Deps, env: Env, msg: msg::QueryMsg) -> StdResult<Binary> {
    contract::query(deps, env, msg)
}

Voila, that's all that is needed to update the state!

First, we need to transform the vector of strings into the vector of addresses to be stored. We cannot take addresses as a message argument because not every string is a valid address. It might be a bit confusing when we were 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 Item object stores nothing and is just 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.

It is a good time to check if the regression we have passes - try running our tests:

> cargo test

...

running 1 test
test contract::tests::greet_query ... FAILED

failures:

---- contract::tests::greet_query stdout ----
thread 'contract::tests::greet_query' panicked at 'called `Result::unwrap()` on an `Err` value: error executing WasmMsg:
sender: owner
Instantiate { admin: None, code_id: 1, msg: Binary(7b7d), funds: [], label: "Contract" }

Caused by:
    Error parsing into type contract::msg::InstantiateMsg: missing field `admins`', src/contract.rs:80:14
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    contract::tests::greet_query

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass '--lib'

Damn, we broke something! But be calm. Let's start with carefully reading an error message:

Error parsing into type contract::msg::InstantiateMsg: missing field admins', src/contract.rs:80:14

The problem is that in the test, we send an empty instantiation message in our test, but right now, our endpoint expects to have an admin field. Multi-test framework tests contract from the entry point to results, so sending messages using MT functions first serializes them. Then the contract deserializes them on the entry. But now it tries to deserialize the empty JSON to some non-empty message! We can quickly fix it by updating the test:

use crate::msg::{GreetResp, InstantiateMsg, QueryMsg};
use crate::state::ADMINS;
use cosmwasm_std::{
    to_binary, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdResult,
};

pub fn instantiate(
    deps: DepsMut,
    _env: Env,
    _info: MessageInfo,
    msg: InstantiateMsg,
) -> StdResult<Response> {
    let admins: StdResult<Vec<_>> = msg
        .admins
        .into_iter()
        .map(|addr| deps.api.addr_validate(&addr))
        .collect();
    ADMINS.save(deps.storage, &admins?)?;

    Ok(Response::new())
}

pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
    use QueryMsg::*;

    match msg {
        Greet {} => to_binary(&query::greet()?),
        AdminsList {} => to_binary(&query::admins_list(deps)?),
    }
}

#[allow(dead_code)]
pub fn execute(_deps: DepsMut, _env: Env, _info: MessageInfo, _msg: Empty) -> StdResult<Response> {
    unimplemented!()
}

mod query {
    use crate::msg::AdminsListResp;

    use super::*;

    pub fn greet() -> StdResult<GreetResp> {
        let resp = GreetResp {
            message: "Hello World".to_owned(),
        };

        Ok(resp)
    }

    pub fn admins_list(deps: Deps) -> StdResult<AdminsListResp> {
        let admins = ADMINS.load(deps.storage)?;
        let resp = AdminsListResp { admins };
        Ok(resp)
    }
}

#[cfg(test)]
mod tests {
    use cosmwasm_std::Addr;
    use cw_multi_test::{App, ContractWrapper, Executor};

    use super::*;

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

        let code = ContractWrapper::new(execute, instantiate, query);
        let code_id = app.store_code(Box::new(code));

        let addr = app
            .instantiate_contract(
                code_id,
                Addr::unchecked("owner"),
                &InstantiateMsg { admins: vec![] },
                &[],
                "Contract",
                None,
            )
            .unwrap();

        let resp: GreetResp = app
            .wrap()
            .query_wasm_smart(addr, &QueryMsg::Greet {})
            .unwrap();

        assert_eq!(
            resp,
            GreetResp {
                message: "Hello World".to_owned()
            }
        );
    }
}

Testing state

When the state is initialized, we want a way to test it. We want to provide a query to check if the instantiation affects the state. Just create a simple one listing all admins. Start with adding a variant for query message:

use cosmwasm_std::Addr;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct InstantiateMsg {
    pub admins: Vec<Addr>,
}

#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct GreetResp {
    pub message: String,
}

#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub enum QueryMsg {
    Greet {},
    AdminsList {},
}

And implement it:

use crate::msg::{AdminsListResp, GreetResp, InstantiateMsg, QueryMsg};
use crate::state::ADMINS;
use cosmwasm_std::{
    to_binary, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdResult,
};

pub fn instantiate(
    deps: DepsMut,
    _env: Env,
    _info: MessageInfo,
    msg: InstantiateMsg,
) -> StdResult<Response> {
    let admins: StdResult<Vec<_>> = msg
        .admins
        .into_iter()
        .map(|addr| deps.api.addr_validate(&addr))
        .collect();
    ADMINS.save(deps.storage, &admins?)?;

    Ok(Response::new())
}

pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
    use QueryMsg::*;

    match msg {
        Greet {} => to_binary(&query::greet()?),
        AdminsList {} => to_binary(&query::admins_list(deps)?),
    }
}
 
#[allow(dead_code)]
pub fn execute(_deps: DepsMut, _env: Env, _info: MessageInfo, _msg: Empty) -> StdResult<Response> {
    unimplemented!()
}

mod query {
   use super::*;

   pub fn greet() -> StdResult<GreetResp> {
       let resp = GreetResp {
           message: "Hello World".to_owned(),
       };

       Ok(resp)
   }

    pub fn admins_list(deps: Deps) -> StdResult<AdminsListResp> {
        let admins = ADMINS.load(deps.storage)?;
        let resp = AdminsListResp { admins };
        Ok(resp)
    }
}

#[cfg(test)]
mod tests {
    use cosmwasm_std::Addr;
    use cw_multi_test::{App, ContractWrapper, Executor};

    use super::*;

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

       let code = ContractWrapper::new(execute, instantiate, query);
       let code_id = app.store_code(Box::new(code));

       let addr = app
           .instantiate_contract(
               code_id,
               Addr::unchecked("owner"),
               &InstantiateMsg { admins: vec![] },
               &[],
               "Contract",
               None,
           )
           .unwrap();

       let resp: GreetResp = app
           .wrap()
           .query_wasm_smart(addr, &QueryMsg::Greet {})
           .unwrap();

       assert_eq!(
           resp,
           GreetResp {
               message: "Hello World".to_owned()
           }
       );
    }
}

Now when we have the tools to test the instantiation, let's write a test case:

use crate::msg::{AdminsListResp, GreetResp, InstantiateMsg, QueryMsg};
use crate::state::ADMINS;
use cosmwasm_std::{
    to_binary, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdResult,
};

pub fn instantiate(
    deps: DepsMut,
    _env: Env,
    _info: MessageInfo,
    msg: InstantiateMsg,
) -> StdResult<Response> {
    let admins: StdResult<Vec<_>> = msg
        .admins
        .into_iter()
        .map(|addr| deps.api.addr_validate(&addr))
        .collect();
    ADMINS.save(deps.storage, &admins?)?;

    Ok(Response::new())
}

pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
    use QueryMsg::*;

    match msg {
        Greet {} => to_binary(&query::greet()?),
        AdminsList {} => to_binary(&query::admins_list(deps)?),
    }
}

#[allow(dead_code)]
pub fn execute(_deps: DepsMut, _env: Env, _info: MessageInfo, _msg: Empty) -> StdResult<Response> {
    unimplemented!()
}

mod query {
    use super::*;

    pub fn greet() -> StdResult<GreetResp> {
        let resp = GreetResp {
            message: "Hello World".to_owned(),
        };

        Ok(resp)
    }

    pub fn admins_list(deps: Deps) -> StdResult<AdminsListResp> {
        let admins = ADMINS.load(deps.storage)?;
        let resp = AdminsListResp { admins };
        Ok(resp)
    }
}

#[cfg(test)]
mod tests {
    use cosmwasm_std::Addr;
    use cw_multi_test::{App, ContractWrapper, Executor};

    use super::*;

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

        let code = ContractWrapper::new(execute, instantiate, query);
        let code_id = app.store_code(Box::new(code));

        let addr = app
            .instantiate_contract(
                code_id,
                Addr::unchecked("owner"),
                &InstantiateMsg { admins: vec![] },
                &[],
                "Contract",
                None,
            )
            .unwrap();

        let resp: AdminsListResp = app
            .wrap()
            .query_wasm_smart(addr, &QueryMsg::AdminsList {})
            .unwrap();

        assert_eq!(resp, AdminsListResp { admins: vec![] });

        let addr = app
            .instantiate_contract(
                code_id,
                Addr::unchecked("owner"),
                &InstantiateMsg {
                    admins: vec!["admin1".to_owned(), "admin2".to_owned()],
                },
                &[],
                "Contract 2",
                None,
            )
            .unwrap();

        let resp: AdminsListResp = app
            .wrap()
            .query_wasm_smart(addr, &QueryMsg::AdminsList {})
            .unwrap();

        assert_eq!(
            resp,
            AdminsListResp {
                admins: vec![Addr::unchecked("admin1"), Addr::unchecked("admin2")],
            }
        );
    }

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

        let code = ContractWrapper::new(execute, instantiate, query);
        let code_id = app.store_code(Box::new(code));

        let addr = app
            .instantiate_contract(
                code_id,
                Addr::unchecked("owner"),
                &InstantiateMsg { admins: vec![] },
                &[],
                "Contract",
                None,
            )
            .unwrap();

        let resp: GreetResp = app
            .wrap()
            .query_wasm_smart(addr, &QueryMsg::Greet {})
            .unwrap();

        assert_eq!(
            resp,
            GreetResp {
                message: "Hello World".to_owned()
            }
        );
    }
}

The test is simple - instantiate the contract twice with different initial admins, and ensure the query result is proper each time. This is often the way we test our contract - we execute bunch o messages on the contract, and then we query it for some data, verifying if query responses are like expected.

We are doing a pretty good job developing our contract. Now it is time to use the state and allow for some executions.

Execution messages

We went through instantiate and query messages. It is finally time to introduce the last basic entry point - the execute messages. It is similar to what we have done so far that I expect this to be just chilling and revisiting our knowledge. I encourage you to try implementing what I am describing here on your own as an exercise - without checking out the source code.

The idea of the contract will be easy - every contract admin would be eligible to call two execute messages:

  • AddMembers message would allow the admin to add another address to the admin's list
  • Leave would allow and admin to remove himself from the list

Not too complicated. Let's go coding. Start with defining messages:

use cosmwasm_std::Addr;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct InstantiateMsg {
    pub admins: Vec<String>,
}

#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub enum ExecuteMsg {
    AddMembers { admins: Vec<String> },
    Leave {},
}

#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct GreetResp {
    pub message: String,
}

#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct AdminsListResp {
    pub admins: Vec<Addr>,
}

#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub enum QueryMsg {
    Greet {},
    AdminsList {},
}

And implement entry point:

use crate::msg::{AdminsListResp, ExecuteMsg, GreetResp, InstantiateMsg, QueryMsg};
use crate::state::ADMINS;
use cosmwasm_std::{to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult};

pub fn instantiate(
    deps: DepsMut,
    _env: Env,
    _info: MessageInfo,
    msg: InstantiateMsg,
) -> StdResult<Response> {
    let admins: StdResult<Vec<_>> = msg
        .admins
        .into_iter()
        .map(|addr| deps.api.addr_validate(&addr))
        .collect();
    ADMINS.save(deps.storage, &admins?)?;

    Ok(Response::new())
}

pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
    use QueryMsg::*;

    match msg {
        Greet {} => to_binary(&query::greet()?),
        AdminsList {} => to_binary(&query::admins_list(deps)?),
    }
}
 
#[allow(dead_code)]
pub fn execute(
    deps: DepsMut,
    _env: Env,
    info: MessageInfo,
    msg: ExecuteMsg,
) -> StdResult<Response> {
    use ExecuteMsg::*;

    match msg {
        AddMembers { admins } => exec::add_members(deps, info, admins),
        Leave {} => exec::leave(deps, info),
    }
}

mod exec {
    use cosmwasm_std::StdError;

    use super::*;

    pub fn add_members(
        deps: DepsMut,
        info: MessageInfo,
        admins: Vec<String>,
    ) -> StdResult<Response> {
        let mut curr_admins = ADMINS.load(deps.storage)?;
        if !curr_admins.contains(&info.sender) {
            return Err(StdError::generic_err("Unauthorised access"));
        }

        let admins: StdResult<Vec<_>> = admins
            .into_iter()
            .map(|addr| deps.api.addr_validate(&addr))
            .collect();

        curr_admins.append(&mut admins?);
        ADMINS.save(deps.storage, &curr_admins)?;

        Ok(Response::new())
    }

    pub fn leave(deps: DepsMut, info: MessageInfo) -> StdResult<Response> {
        ADMINS.update(deps.storage, move |admins| -> StdResult<_> {
            let admins = admins
                .into_iter()
                .filter(|admin| *admin != info.sender)
                .collect();
            Ok(admins)
        })?;

        Ok(Response::new())
    }
}

mod query {
    use super::*;

    pub fn greet() -> StdResult<GreetResp> {
        let resp = GreetResp {
            message: "Hello World".to_owned(),
        };

        Ok(resp)
    }

    pub fn admins_list(deps: Deps) -> StdResult<AdminsListResp> {
        let admins = ADMINS.load(deps.storage)?;
        let resp = AdminsListResp { admins };
        Ok(resp)
    }
}

#[cfg(test)]
mod tests {
    use cosmwasm_std::Addr;
    use cw_multi_test::{App, ContractWrapper, Executor};

    use crate::msg::AdminsListResp;

    use super::*;

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

        let code = ContractWrapper::new(execute, instantiate, query);
        let code_id = app.store_code(Box::new(code));

        let addr = app
            .instantiate_contract(
                code_id,
                Addr::unchecked("owner"),
                &InstantiateMsg { admins: vec![] },
                &[],
                "Contract",
                None,
            )
            .unwrap();

        let resp: AdminsListResp = app
            .wrap()
            .query_wasm_smart(addr, &QueryMsg::AdminsList {})
            .unwrap();

        assert_eq!(resp, AdminsListResp { admins: vec![] });

        let addr = app
            .instantiate_contract(
                code_id,
                Addr::unchecked("owner"),
                &InstantiateMsg {
                    admins: vec!["admin1".to_owned(), "admin2".to_owned()],
                },
                &[],
                "Contract 2",
                None,
            )
            .unwrap();

        let resp: AdminsListResp = app
            .wrap()
            .query_wasm_smart(addr, &QueryMsg::AdminsList {})
            .unwrap();

        assert_eq!(
            resp,
            AdminsListResp {
                admins: vec![Addr::unchecked("admin1"), Addr::unchecked("admin2")],
            }
        );
    }

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

        let code = ContractWrapper::new(execute, instantiate, query);
        let code_id = app.store_code(Box::new(code));

        let addr = app
            .instantiate_contract(
                code_id,
                Addr::unchecked("owner"),
                &InstantiateMsg { admins: vec![] },
                &[],
                "Contract",
                None,
            )
            .unwrap();

        let resp: GreetResp = app
            .wrap()
            .query_wasm_smart(addr, &QueryMsg::Greet {})
            .unwrap();

        assert_eq!(
            resp,
            GreetResp {
                message: "Hello World".to_owned()
            }
        );
    }
}

The entry point itself also has to be created in src/lib.rs:

use cosmwasm_std::{entry_point, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult};
use msg::{ExecuteMsg, InstantiateMsg, QueryMsg};

mod contract;
mod msg;
mod state;

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

#[entry_point]
pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> StdResult<Response> {
    contract::execute(deps, env, info, msg)
}

#[entry_point]
pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
    contract::query(deps, env, msg)
}

There are a couple of new things, but nothing significant. First is how do I reach the message sender to verify he is an admin or remove him from the list - I used the info.sender field of MessageInfo, which is how it looks like - the member. As the message is always sent from the proper address, the sender is already of the Addr type - no need to validate it. Another new thing is the update function on an Item - it makes a read and update of an entity potentially more efficient. It is possible to do it by reading admins first, then updating and storing the result.

You probably noticed that when working with Item, we always assume something is there. But nothing forces us to initialize the ADMINS value on instantiation! So what happens there? Well, both load and update functions would return an error. But there is a may_load function, which returns StdResult<Option<T>> - it would return Ok(None) in case of empty storage. There is even a possibility to remove an existing item from storage with the remove function.

One thing to improve is error handling. While validating the sender to be admin, we are returning some arbitrary string as an error. We can do better.

Error handling

In our contract, we now have an error situation when a user tries to execute AddMembers not being an admin himself. There is no proper error case in StdError to report this situation, so we have to return a generic error with a message. It is not the best approach.

For error reporting, we encourage using thiserror crate. Start with updating your dependencies:

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

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

[dependencies]
cosmwasm-std = { version = "1.0.0-beta8", features = ["staking"] }
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
cw-storage-plus = "0.13.4"
thiserror = "1"

[dev-dependencies]
cw-multi-test = "0.13.4"

Now we define an error type in src/error.rs:

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

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

We also need to add the new module to src/lib.rs:

use cosmwasm_std::{entry_point, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult};
use msg::{ExecuteMsg, InstantiateMsg, QueryMsg};

mod contract;
mod error;
mod msg;
mod state;

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

#[entry_point]
pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> StdResult<Response> {
    contract::execute(deps, env, info, msg)
}

#[entry_point]
pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
    contract::query(deps, env, msg)
}

Using thiserror we define errors like a simple enum, and the crate ensures that the type implements std::error::Error trait. A very nice feature of this crate is the inline definition of Display trait by an #[error] attribute. Also, another helpful thing is the #[from] attribute, which automatically generates proper From implementation, so it is easy to use ? operator with thiserror types.

Now update the execute endpoint to use our new error type:

use crate::error::ContractError;
use crate::msg::{AdminsListResp, ExecuteMsg, GreetResp, InstantiateMsg, QueryMsg};
use crate::state::ADMINS;
use cosmwasm_std::{to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult};

pub fn instantiate(
    deps: DepsMut,
    _env: Env,
    _info: MessageInfo,
    msg: InstantiateMsg,
) -> StdResult<Response> {
    let admins: StdResult<Vec<_>> = msg
        .admins
        .into_iter()
        .map(|addr| deps.api.addr_validate(&addr))
        .collect();
    ADMINS.save(deps.storage, &admins?)?;

    Ok(Response::new())
}

pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
    use QueryMsg::*;

    match msg {
        Greet {} => to_binary(&query::greet()?),
        AdminsList {} => to_binary(&query::admins_list(deps)?),
    }
}
 
pub fn execute(
    deps: DepsMut,
    _env: Env,
    info: MessageInfo,
    msg: ExecuteMsg,
) -> Result<Response, ContractError> {
    use ExecuteMsg::*;

    match msg {
        AddMembers { admins } => exec::add_members(deps, info, admins),
        Leave {} => exec::leave(deps, info).map_err(Into::into),
    }
}

mod exec {
    use super::*;

    pub fn add_members(
        deps: DepsMut,
        info: MessageInfo,
        admins: Vec<String>,
    ) -> Result<Response, ContractError> {
        let mut curr_admins = ADMINS.load(deps.storage)?;
        if !curr_admins.contains(&info.sender) {
            return Err(ContractError::Unauthorized {
                sender: info.sender,
            });
        }

        let admins: StdResult<Vec<_>> = admins
            .into_iter()
            .map(|addr| deps.api.addr_validate(&addr))
            .collect();

        curr_admins.append(&mut admins?);
        ADMINS.save(deps.storage, &curr_admins)?;

        Ok(Response::new())
    }

    pub fn leave(deps: DepsMut, info: MessageInfo) -> StdResult<Response> {
        ADMINS.update(deps.storage, move |admins| -> StdResult<_> {
            let admins = admins
                .into_iter()
                .filter(|admin| *admin != info.sender)
                .collect();
            Ok(admins)
        })?;

        Ok(Response::new())
    }
}

mod query {
    use super::*;

    pub fn greet() -> StdResult<GreetResp> {
        let resp = GreetResp {
            message: "Hello World".to_owned(),
        };

        Ok(resp)
    }

    pub fn admins_list(deps: Deps) -> StdResult<AdminsListResp> {
        let admins = ADMINS.load(deps.storage)?;
        let resp = AdminsListResp { admins };
        Ok(resp)
    }
}

#[cfg(test)]
mod tests {
    use cosmwasm_std::Addr;
    use cw_multi_test::{App, ContractWrapper, Executor};

    use crate::msg::AdminsListResp;

    use super::*;

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

        let code = ContractWrapper::new(execute, instantiate, query);
        let code_id = app.store_code(Box::new(code));

        let addr = app
            .instantiate_contract(
                code_id,
                Addr::unchecked("owner"),
                &InstantiateMsg { admins: vec![] },
                &[],
                "Contract",
                None,
            )
            .unwrap();

        let resp: AdminsListResp = app
            .wrap()
            .query_wasm_smart(addr, &QueryMsg::AdminsList {})
            .unwrap();

        assert_eq!(resp, AdminsListResp { admins: vec![] });

        let addr = app
            .instantiate_contract(
                code_id,
                Addr::unchecked("owner"),
                &InstantiateMsg {
                    admins: vec!["admin1".to_owned(), "admin2".to_owned()],
                },
                &[],
                "Contract 2",
                None,
            )
            .unwrap();

        let resp: AdminsListResp = app
            .wrap()
            .query_wasm_smart(addr, &QueryMsg::AdminsList {})
            .unwrap();

        assert_eq!(
            resp,
            AdminsListResp {
                admins: vec![Addr::unchecked("admin1"), Addr::unchecked("admin2")],
            }
        );
    }

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

        let code = ContractWrapper::new(execute, instantiate, query);
        let code_id = app.store_code(Box::new(code));

        let addr = app
            .instantiate_contract(
                code_id,
                Addr::unchecked("owner"),
                &InstantiateMsg { admins: vec![] },
                &[],
                "Contract",
                None,
            )
            .unwrap();

        let resp: GreetResp = app
            .wrap()
            .query_wasm_smart(addr, &QueryMsg::Greet {})
            .unwrap();

        assert_eq!(
            resp,
            GreetResp {
                message: "Hello World".to_owned()
            }
        );
    }
}

The entry point return type also has to be updated:

use cosmwasm_std::{entry_point, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult};
use error::ContractError;
use msg::{ExecuteMsg, InstantiateMsg, QueryMsg};

mod contract;
mod error;
mod msg;
mod state;

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

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

#[entry_point]
pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
    contract::query(deps, env, msg)
}

Custom error and multi-test

Using proper custom error type has one nice upside - multi-test is maintaining error type using the anyhow crate. It is a sibling of thiserror, designed to implement type-erased errors in a way that allows getting the original error back.

Let's write a test that verifies that a non-admin cannot add himself to a list:

use crate::error::ContractError;
use crate::msg::{AdminsListResp, ExecuteMsg, GreetResp, InstantiateMsg, QueryMsg};
use crate::state::ADMINS;
use cosmwasm_std::{to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult};

pub fn instantiate(
    deps: DepsMut,
    _env: Env,
    _info: MessageInfo,
    msg: InstantiateMsg,
) -> StdResult<Response> {
    let admins: StdResult<Vec<_>> = msg
        .admins
        .into_iter()
        .map(|addr| deps.api.addr_validate(&addr))
        .collect();
    ADMINS.save(deps.storage, &admins?)?;

    Ok(Response::new())
}

pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
    use QueryMsg::*;

    match msg {
        Greet {} => to_binary(&query::greet()?),
        AdminsList {} => to_binary(&query::admins_list(deps)?),
    }
}

pub fn execute(
    deps: DepsMut,
    _env: Env,
    info: MessageInfo,
    msg: ExecuteMsg,
) -> Result<Response, ContractError> {
    use ExecuteMsg::*;

    match msg {
        AddMembers { admins } => exec::add_members(deps, info, admins),
        Leave {} => exec::leave(deps, info).map_err(Into::into),
    }
}

mod exec {
    use super::*;

    pub fn add_members(
        deps: DepsMut,
        info: MessageInfo,
        admins: Vec<String>,
    ) -> Result<Response, ContractError> {
        let mut curr_admins = ADMINS.load(deps.storage)?;
        if !curr_admins.contains(&info.sender) {
            return Err(ContractError::Unauthorized {
                sender: info.sender,
            });
        }

        let admins: StdResult<Vec<_>> = admins
            .into_iter()
            .map(|addr| deps.api.addr_validate(&addr))
            .collect();

        curr_admins.append(&mut admins?);
        ADMINS.save(deps.storage, &curr_admins)?;

        Ok(Response::new())
    }

    pub fn leave(deps: DepsMut, info: MessageInfo) -> StdResult<Response> {
        ADMINS.update(deps.storage, move |admins| -> StdResult<_> {
            let admins = admins
                .into_iter()
                .filter(|admin| *admin != info.sender)
                .collect();
            Ok(admins)
        })?;

        Ok(Response::new())
    }
}

mod query {
    use super::*;

    pub fn greet() -> StdResult<GreetResp> {
        let resp = GreetResp {
            message: "Hello World".to_owned(),
        };

        Ok(resp)
    }

    pub fn admins_list(deps: Deps) -> StdResult<AdminsListResp> {
        let admins = ADMINS.load(deps.storage)?;
        let resp = AdminsListResp { admins };
        Ok(resp)
    }
}

#[cfg(test)]
mod tests {
    use cosmwasm_std::Addr;
    use cw_multi_test::{App, ContractWrapper, Executor};

    use crate::msg::AdminsListResp;

    use super::*;

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

        let code = ContractWrapper::new(execute, instantiate, query);
        let code_id = app.store_code(Box::new(code));

        let addr = app
            .instantiate_contract(
                code_id,
                Addr::unchecked("owner"),
                &InstantiateMsg { admins: vec![] },
                &[],
                "Contract",
                None,
            )
            .unwrap();

        let resp: AdminsListResp = app
            .wrap()
            .query_wasm_smart(addr, &QueryMsg::AdminsList {})
            .unwrap();

        assert_eq!(resp, AdminsListResp { admins: vec![] });

        let addr = app
            .instantiate_contract(
                code_id,
                Addr::unchecked("owner"),
                &InstantiateMsg {
                    admins: vec!["admin1".to_owned(), "admin2".to_owned()],
                },
                &[],
                "Contract 2",
                None,
            )
            .unwrap();

        let resp: AdminsListResp = app
            .wrap()
            .query_wasm_smart(addr, &QueryMsg::AdminsList {})
            .unwrap();

        assert_eq!(
            resp,
            AdminsListResp {
                admins: vec![Addr::unchecked("admin1"), Addr::unchecked("admin2")],
            }
        );
    }

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

        let code = ContractWrapper::new(execute, instantiate, query);
        let code_id = app.store_code(Box::new(code));

        let addr = app
            .instantiate_contract(
                code_id,
                Addr::unchecked("owner"),
                &InstantiateMsg { admins: vec![] },
                &[],
                "Contract",
                None,
            )
            .unwrap();

        let resp: GreetResp = app
            .wrap()
            .query_wasm_smart(addr, &QueryMsg::Greet {})
            .unwrap();

        assert_eq!(
            resp,
            GreetResp {
                message: "Hello World".to_owned()
            }
        );
    }

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

        let code = ContractWrapper::new(execute, instantiate, query);
        let code_id = app.store_code(Box::new(code));

        let addr = app
            .instantiate_contract(
                code_id,
                Addr::unchecked("owner"),
                &InstantiateMsg { admins: vec![] },
                &[],
                "Contract",
                None,
            )
            .unwrap();

        let err = app
            .execute_contract(
                Addr::unchecked("user"),
                addr,
                &ExecuteMsg::AddMembers {
                    admins: vec!["user".to_owned()],
                },
                &[],
            )
            .unwrap_err();

        assert_eq!(
            ContractError::Unauthorized {
                sender: Addr::unchecked("user")
            },
            err.downcast().unwrap()
        );
    }
}

Executing a contract is very similar to any other call - we use an execute_contract function. As the execution may fail, we get an error type out of this call, but instead of calling unwrap to extract a value out of it, we expect an error to occur - this is the purpose of the unwrap_err call. Now, as we have an error value, we can check if it matches what we expected with an assert_eq!. There is a slight complication - the error returned from execute_contract is an anyhow::Error error, but we expect it to be a ContractError. Hopefully, as I said before, anyhow errors can recover their original type using the downcast function. The unwrap right after it is needed because downcasting may fail. The reason is that downcast doesn't magically know the type kept in the underlying error. It deduces it by some context - here, it knows we expect it to be a ContractError, because of being compared to it - type elision miracles. But if the underlying error would not be a ContractError, then unwrap would panic.

We just created a simple failure test for execution, but it is not enough to claim the contract is production-ready. All reasonable ok-cases should be covered for that. I encourage you to create some tests and experiment with them as an exercise after this chapter.

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 AddMembers:

use crate::error::ContractError;
use crate::msg::{AdminsListResp, ExecuteMsg, GreetResp, InstantiateMsg, QueryMsg};
use crate::state::ADMINS;
use cosmwasm_std::{
    to_binary, Binary, Deps, DepsMut, Env, Event, MessageInfo, Response, StdResult,
};
 
pub fn instantiate(
    deps: DepsMut,
    _env: Env,
    _info: MessageInfo,
    msg: InstantiateMsg,
) -> StdResult<Response> {
    let admins: StdResult<Vec<_>> = msg
        .admins
        .map(|addr| deps.api.addr_validate(&addr))
        .collect();
    ADMINS.save(deps.storage, &admins?)?;

    Ok(Response::new())
}

pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
    use QueryMsg::*;

    match msg {
        Greet {} => to_binary(&query::greet()?),
        AdminsList {} => to_binary(&query::admins_list(deps)?),
    }
}

pub fn execute(
    deps: DepsMut,
    _env: Env,
    info: MessageInfo,
    msg: ExecuteMsg,
) -> Result<Response, ContractError> {
    use ExecuteMsg::*;

    match msg {
        AddMembers { admins } => exec::add_members(deps, info, admins),
        Leave {} => exec::leave(deps, info).map_err(Into::into),
    }
}

mod exec {
    use super::*;

    pub fn add_members(
        deps: DepsMut,
        info: MessageInfo,
        admins: Vec<String>,
    ) -> Result<Response, ContractError> {
        let mut curr_admins = ADMINS.load(deps.storage)?;
        if !curr_admins.contains(&info.sender) {
            return Err(ContractError::Unauthorized {
                sender: info.sender,
            });
        }

        let events = admins
            .iter()
            .map(|admin| Event::new("admin_added").add_attribute("addr", admin));
        let resp = Response::new()
            .add_events(events)
            .add_attribute("action", "add_members")
            .add_attribute("added_count", admins.len().to_string());

        let admins: StdResult<Vec<_>> = admins
            .into_iter()
            .map(|addr| deps.api.addr_validate(&addr))
            .collect();

        curr_admins.append(&mut admins?);
        ADMINS.save(deps.storage, &curr_admins)?;

        Ok(resp)
    }

    pub fn leave(deps: DepsMut, info: MessageInfo) -> StdResult<Response> {
        ADMINS.update(deps.storage, move |admins| -> StdResult<_> {
            let admins = admins
                .into_iter()
                .filter(|admin| *admin != info.sender)
                .collect();
            Ok(admins)
        })?;

        Ok(Response::new())
    }
}

mod query {
    use super::*;

    pub fn greet() -> StdResult<GreetResp> {
        let resp = GreetResp {
            message: "Hello World".to_owned(),
        };

        Ok(resp)
    }

    pub fn admins_list(deps: Deps) -> StdResult<AdminsListResp> {
        let admins = ADMINS.load(deps.storage)?;
        let resp = AdminsListResp { admins };
        Ok(resp)
    }
}

#[cfg(test)]
mod tests {
    use cosmwasm_std::Addr;
    use cw_multi_test::{App, ContractWrapper, Executor};

    use crate::msg::AdminsListResp;

    use super::*;

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

        let code = ContractWrapper::new(execute, instantiate, query);
        let code_id = app.store_code(Box::new(code));

        let addr = app
            .instantiate_contract(
                code_id,
                Addr::unchecked("owner"),
                &InstantiateMsg { admins: vec![] },
                &[],
                "Contract",
                None,
            )
            .unwrap();

        let resp: AdminsListResp = app
            .wrap()
            .query_wasm_smart(addr, &QueryMsg::AdminsList {})
            .unwrap();

        assert_eq!(resp, AdminsListResp { admins: vec![] });

        let addr = app
            .instantiate_contract(
                code_id,
                Addr::unchecked("owner"),
                &InstantiateMsg {
                    admins: vec!["admin1".to_owned(), "admin2".to_owned()],
                },
                &[],
                "Contract 2",
                None,
            )
            .unwrap();

        let resp: AdminsListResp = app
            .wrap()
            .query_wasm_smart(addr, &QueryMsg::AdminsList {})
            .unwrap();

        assert_eq!(
            resp,
            AdminsListResp {
                admins: vec![Addr::unchecked("admin1"), Addr::unchecked("admin2")],
            }
        );
    }

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

        let code = ContractWrapper::new(execute, instantiate, query);
        let code_id = app.store_code(Box::new(code));

        let addr = app
            .instantiate_contract(
                code_id,
                Addr::unchecked("owner"),
                &InstantiateMsg { admins: vec![] },
                &[],
                "Contract",
                None,
            )
            .unwrap();

        let resp: GreetResp = app
            .wrap()
            .query_wasm_smart(addr, &QueryMsg::Greet {})
            .unwrap();

        assert_eq!(
            resp,
            GreetResp {
                message: "Hello World".to_owned()
            }
        );
    }

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

        let code = ContractWrapper::new(execute, instantiate, query);
        let code_id = app.store_code(Box::new(code));

        let addr = app
            .instantiate_contract(
                code_id,
                Addr::unchecked("owner"),
                &InstantiateMsg { admins: vec![] },
                &[],
                "Contract",
                None,
            )
            .unwrap();

        let err = app
            .execute_contract(
                Addr::unchecked("user"),
                addr,
                &ExecuteMsg::AddMembers {
                    admins: vec!["user".to_owned()],
                },
                &[],
            )
            .unwrap_err();

        assert_eq!(
            ContractError::Unauthorized {
                sender: Addr::unchecked("user")
            },
            err.downcast().unwrap()
        );
    }
}

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 main contract logic. Let's now write single test checking if execution emits events:

use crate::error::ContractError;
use crate::msg::{AdminsListResp, ExecuteMsg, GreetResp, InstantiateMsg, QueryMsg};
use crate::state::ADMINS;
use cosmwasm_std::{
    to_binary, Binary, Deps, DepsMut, Env, Event, MessageInfo, Response, StdResult,
};

pub fn instantiate(
    deps: DepsMut,
    _env: Env,
    _info: MessageInfo,
    msg: InstantiateMsg,
) -> StdResult<Response> {
    let admins: StdResult<Vec<_>> = msg
        .admins
        .into_iter()
        .map(|addr| deps.api.addr_validate(&addr))
        .collect();
    ADMINS.save(deps.storage, &admins?)?;

    Ok(Response::new())
}

pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
    use QueryMsg::*;

    match msg {
        Greet {} => to_binary(&query::greet()?),
        AdminsList {} => to_binary(&query::admins_list(deps)?),
    }
}

pub fn execute(
    deps: DepsMut,
    _env: Env,
    info: MessageInfo,
    msg: ExecuteMsg,
) -> Result<Response, ContractError> {
    use ExecuteMsg::*;

    match msg {
        AddMembers { admins } => exec::add_members(deps, info, admins),
        Leave {} => exec::leave(deps, info).map_err(Into::into),
    }
}

mod exec {
    use super::*;

    pub fn add_members(
        deps: DepsMut,
        info: MessageInfo,
        admins: Vec<String>,
    ) -> Result<Response, ContractError> {
        let mut curr_admins = ADMINS.load(deps.storage)?;
        if !curr_admins.contains(&info.sender) {
            return Err(ContractError::Unauthorized {
                sender: info.sender,
            });
        }

        let events = admins
            .iter()
            .map(|admin| Event::new("admin_added").add_attribute("addr", admin));
        let resp = Response::new()
            .add_events(events)
            .add_attribute("action", "add_members")
            .add_attribute("added_count", admins.len().to_string());

        let admins: StdResult<Vec<_>> = admins
            .into_iter()
            .map(|addr| deps.api.addr_validate(&addr))
            .collect();

        curr_admins.append(&mut admins?);
        ADMINS.save(deps.storage, &curr_admins)?;

        Ok(resp)
    }

    pub fn leave(deps: DepsMut, info: MessageInfo) -> StdResult<Response> {
        ADMINS.update(deps.storage, move |admins| -> StdResult<_> {
            let admins = admins
                .into_iter()
                .filter(|admin| *admin != info.sender)
                .collect();
            Ok(admins)
        })?;

        Ok(Response::new())
    }
}

mod query {
    use super::*;

    pub fn greet() -> StdResult<GreetResp> {
        let resp = GreetResp {
            message: "Hello World".to_owned(),
        };

        Ok(resp)
    }

    pub fn admins_list(deps: Deps) -> StdResult<AdminsListResp> {
        let admins = ADMINS.load(deps.storage)?;
        let resp = AdminsListResp { admins };
        Ok(resp)
    }
}

#[cfg(test)]
mod tests {
    use cosmwasm_std::Addr;
    use cw_multi_test::{App, ContractWrapper, Executor};

    use crate::msg::AdminsListResp;

    use super::*;

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

        let code = ContractWrapper::new(execute, instantiate, query);
        let code_id = app.store_code(Box::new(code));

        let addr = app
            .instantiate_contract(
                code_id,
                Addr::unchecked("owner"),
                &InstantiateMsg { admins: vec![] },
                &[],
                "Contract",
                None,
            )
            .unwrap();

        let resp: AdminsListResp = app
            .wrap()
            .query_wasm_smart(addr, &QueryMsg::AdminsList {})
            .unwrap();

        assert_eq!(resp, AdminsListResp { admins: vec![] });

        let addr = app
            .instantiate_contract(
                code_id,
                Addr::unchecked("owner"),
                &InstantiateMsg {
                    admins: vec!["admin1".to_owned(), "admin2".to_owned()],
                },
                &[],
                "Contract 2",
                None,
            )
            .unwrap();

        let resp: AdminsListResp = app
            .wrap()
            .query_wasm_smart(addr, &QueryMsg::AdminsList {})
            .unwrap();

        assert_eq!(
            resp,
            AdminsListResp {
                admins: vec![Addr::unchecked("admin1"), Addr::unchecked("admin2")],
            }
        );
    }

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

        let code = ContractWrapper::new(execute, instantiate, query);
        let code_id = app.store_code(Box::new(code));

        let addr = app
            .instantiate_contract(
                code_id,
                Addr::unchecked("owner"),
                &InstantiateMsg { admins: vec![] },
                &[],
                "Contract",
                None,
            )
            .unwrap();

        let resp: GreetResp = app
            .wrap()
            .query_wasm_smart(addr, &QueryMsg::Greet {})
            .unwrap();

        assert_eq!(
            resp,
            GreetResp {
                message: "Hello World".to_owned()
            }
        );
    }

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

        let code = ContractWrapper::new(execute, instantiate, query);
        let code_id = app.store_code(Box::new(code));

        let addr = app
            .instantiate_contract(
                code_id,
                Addr::unchecked("owner"),
                &InstantiateMsg { admins: vec![] },
                &[],
                "Contract",
                None,
            )
            .unwrap();

        let err = app
            .execute_contract(
                Addr::unchecked("user"),
                addr,
                &ExecuteMsg::AddMembers {
                    admins: vec!["user".to_owned()],
                },
                &[],
            )
            .unwrap_err();

        assert_eq!(
            ContractError::Unauthorized {
                sender: Addr::unchecked("user")
            },
            err.downcast().unwrap()
        );
    }

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

        let code = ContractWrapper::new(execute, instantiate, query);
        let code_id = app.store_code(Box::new(code));

        let addr = app
            .instantiate_contract(
                code_id,
                Addr::unchecked("owner"),
                &InstantiateMsg {
                    admins: vec!["owner".to_owned()],
                },
                &[],
                "Contract",
                None,
            )
            .unwrap();

        let resp = app
            .execute_contract(
                Addr::unchecked("owner"),
                addr,
                &ExecuteMsg::AddMembers {
                    admins: vec!["user".to_owned()],
                },
                &[],
            )
            .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_members"
        );
        assert_eq!(
            wasm.attributes
                .iter()
                .find(|attr| attr.key == "added_count")
                .unwrap()
                .value,
            "1"
        );

        let admin_added: Vec<_> = resp
            .events
            .iter()
            .filter(|ev| ev.ty == "wasm-admin_added")
            .collect();
        assert_eq!(admin_added.len(), 1);

        assert_eq!(
            admin_added[0]
                .attributes
                .iter()
                .find(|attr| attr.key == "addr")
                .unwrap()
                .value,
            "user"
        );
    }
}

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, even types are prefixed with "wasm-" - it may not be a huge problem, but it doesn't clarify verification. But the problem is, how layered events structure 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 relies on. 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 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 hard work performed by admins. We will create a new message - Donate, which will be used by anyone 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 cosmwasm_std::Addr;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct InstantiateMsg {
    pub admins: Vec<String>,
    pub donation_denom: String,
}

#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub enum ExecuteMsg {
    AddMembers { admins: Vec<String> },
    Leave {},
    Donate {},
}

#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct GreetResp {
    pub message: String,
}

#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct AdminsListResp {
    pub admins: Vec<Addr>,
}

#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub enum QueryMsg {
    Greet {},
    AdminsList {},
}

We also need to add a new state part, to keep the donation_denom:

use cosmwasm_std::Addr;
use cw_storage_plus::Item;

pub const ADMINS: Item<Vec<Addr>> = Item::new("admins");
pub const DONATION_DENOM: Item<String> = Item::new("donation_denom");

And instantiate it properly:

use crate::error::ContractError;
use crate::msg::{AdminsListResp, ExecuteMsg, GreetResp, InstantiateMsg, QueryMsg};
use crate::state::{ADMINS, DONATION_DENOM};
use cosmwasm_std::{
    to_binary, Binary, Deps, DepsMut, Env, Event, MessageInfo, Response, StdResult,
};

pub fn instantiate(
    deps: DepsMut,
    _env: Env,
    _info: MessageInfo,
    msg: InstantiateMsg,
) -> StdResult<Response> {
    let admins: StdResult<Vec<_>> = msg
        .admins
        .into_iter()
        .map(|addr| deps.api.addr_validate(&addr))
        .collect();
    ADMINS.save(deps.storage, &admins?)?;
    DONATION_DENOM.save(deps.storage, &msg.donation_denom)?;

    Ok(Response::new())
}

pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
    use QueryMsg::*;

    match msg {
        Greet {} => to_binary(&query::greet()?),
        AdminsList {} => to_binary(&query::admins_list(deps)?),
    }
}

pub fn execute(
    deps: DepsMut,
    _env: Env,
    info: MessageInfo,
    msg: ExecuteMsg,
) -> Result<Response, ContractError> {
    use ExecuteMsg::*;

    match msg {
        AddMembers { admins } => exec::add_members(deps, info, admins),
        Leave {} => exec::leave(deps, info).map_err(Into::into),
    }
}

mod exec {
    use super::*;

    pub fn add_members(
        deps: DepsMut,
        info: MessageInfo,
        admins: Vec<String>,
    ) -> Result<Response, ContractError> {
        let mut curr_admins = ADMINS.load(deps.storage)?;
        if !curr_admins.contains(&info.sender) {
            return Err(ContractError::Unauthorized {
                sender: info.sender,
            });
        }

        let events = admins
            .iter()
            .map(|admin| Event::new("admin_added").add_attribute("addr", admin));
        let resp = Response::new()
            .add_events(events)
            .add_attribute("action", "add_members")
            .add_attribute("added_count", admins.len().to_string());

        let admins: StdResult<Vec<_>> = admins
            .into_iter()
            .map(|addr| deps.api.addr_validate(&addr))
            .collect();

        curr_admins.append(&mut admins?);
        ADMINS.save(deps.storage, &curr_admins)?;

        Ok(resp)
    }

    pub fn leave(deps: DepsMut, info: MessageInfo) -> StdResult<Response> {
        ADMINS.update(deps.storage, move |admins| -> StdResult<_> {
            let admins = admins
                .into_iter()
                .filter(|admin| *admin != info.sender)
                .collect();
            Ok(admins)
        })?;

        Ok(Response::new())
    }
}

mod query {
    use super::*;

    pub fn greet() -> StdResult<GreetResp> {
        let resp = GreetResp {
            message: "Hello World".to_owned(),
        };

        Ok(resp)
    }

    pub fn admins_list(deps: Deps) -> StdResult<AdminsListResp> {
        let admins = ADMINS.load(deps.storage)?;
        let resp = AdminsListResp { admins };
        Ok(resp)
    }
}

#[cfg(test)]
mod tests {
    use cosmwasm_std::Addr;
    use cw_multi_test::{App, ContractWrapper, Executor};

    use crate::msg::AdminsListResp;

    use super::*;

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

        let code = ContractWrapper::new(execute, instantiate, query);
        let code_id = app.store_code(Box::new(code));

        let addr = app
            .instantiate_contract(
                code_id,
                Addr::unchecked("owner"),
                &InstantiateMsg { admins: vec![] },
                &[],
                "Contract",
                None,
            )
            .unwrap();

        let resp: AdminsListResp = app
            .wrap()
            .query_wasm_smart(addr, &QueryMsg::AdminsList {})
            .unwrap();

        assert_eq!(resp, AdminsListResp { admins: vec![] });

        let addr = app
            .instantiate_contract(
                code_id,
                Addr::unchecked("owner"),
                &InstantiateMsg {
                    admins: vec!["admin1".to_owned(), "admin2".to_owned()],
                },
                &[],
                "Contract 2",
                None,
            )
            .unwrap();

        let resp: AdminsListResp = app
            .wrap()
            .query_wasm_smart(addr, &QueryMsg::AdminsList {})
            .unwrap();

        assert_eq!(
            resp,
            AdminsListResp {
                admins: vec![Addr::unchecked("admin1"), Addr::unchecked("admin2")],
            }
        );
    }

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

        let code = ContractWrapper::new(execute, instantiate, query);
        let code_id = app.store_code(Box::new(code));

        let addr = app
            .instantiate_contract(
                code_id,
                Addr::unchecked("owner"),
                &InstantiateMsg { admins: vec![] },
                &[],
                "Contract",
                None,
            )
            .unwrap();

        let resp: GreetResp = app
            .wrap()
            .query_wasm_smart(addr, &QueryMsg::Greet {})
            .unwrap();

        assert_eq!(
            resp,
            GreetResp {
                message: "Hello World".to_owned()
            }
        );
    }

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

        let code = ContractWrapper::new(execute, instantiate, query);
        let code_id = app.store_code(Box::new(code));

        let addr = app
            .instantiate_contract(
                code_id,
                Addr::unchecked("owner"),
                &InstantiateMsg { admins: vec![] },
                &[],
                "Contract",
                None,
            )
            .unwrap();

        let err = app
            .execute_contract(
                Addr::unchecked("user"),
                addr,
                &ExecuteMsg::AddMembers {
                    admins: vec!["user".to_owned()],
                },
                &[],
            )
            .unwrap_err();

        assert_eq!(
            ContractError::Unauthorized {
                sender: Addr::unchecked("user")
            },
            err.downcast().unwrap()
        );
    }

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

        let code = ContractWrapper::new(execute, instantiate, query);
        let code_id = app.store_code(Box::new(code));

        let addr = app
            .instantiate_contract(
                code_id,
                Addr::unchecked("owner"),
                &InstantiateMsg {
                    admins: vec!["owner".to_owned()],
                },
                &[],
                "Contract",
                None,
            )
            .unwrap();

        let resp = app
            .execute_contract(
                Addr::unchecked("owner"),
                addr,
                &ExecuteMsg::AddMembers {
                    admins: vec!["user".to_owned()],
                },
                &[],
            )
            .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_members"
        );
        assert_eq!(
            wasm.attributes
                .iter()
                .find(|attr| attr.key == "added_count")
                .unwrap()
                .value,
            "1"
        );

        let admin_added: Vec<_> = resp
            .events
            .iter()
            .filter(|ev| ev.ty == "wasm-admin_added")
            .collect();
        assert_eq!(admin_added.len(), 1);

        assert_eq!(
            admin_added[0]
                .attributes
                .iter()
                .find(|attr| attr.key == "addr")
                .unwrap()
                .value,
            "user"
        );
    }
}

What also needs some corrections are tests - instantiate messages have a new field. I leave it to you as an exercise. Now we have everything we need to implement donating funds to admins. First, a minor update to the Cargo.toml - we will use an additional utility crate:

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

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

[features]
library = []

[dependencies]
cosmwasm-std = { version = "1.0.0-beta8", features = ["staking"] }
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
cw-storage-plus = "0.13.4"
thiserror = "1"
schemars = "0.8.1"
cw-utils = "0.13"

[dev-dependencies]
cw-multi-test = "0.13.4"
cosmwasm-schema = { version = "1.0.0" }

Then we can implement the donate handler:

use crate::error::ContractError;
use crate::msg::{AdminsListResp, ExecuteMsg, GreetResp, InstantiateMsg, QueryMsg};
use crate::state::{ADMINS, DONATION_DENOM};
use cosmwasm_std::{
    coins, to_binary, BankMsg, Binary, Deps, DepsMut, Env, Event, MessageInfo,
    Response, StdResult,
};
 
pub fn instantiate(
    deps: DepsMut,
    _env: Env,
    _info: MessageInfo,
    msg: InstantiateMsg,
) -> StdResult<Response> {
    let admins: StdResult<Vec<_>> = msg
        .admins
        .into_iter()
        .map(|addr| deps.api.addr_validate(&addr))
        .collect();
    ADMINS.save(deps.storage, &admins?)?;
    DONATION_DENOM.save(deps.storage, &msg.donation_denom)?;

    Ok(Response::new())
}

pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
    use QueryMsg::*;

    match msg {
        Greet {} => to_binary(&query::greet()?),
        AdminsList {} => to_binary(&query::admins_list(deps)?),
    }
}

pub fn execute(
    deps: DepsMut,
    _env: Env,
    info: MessageInfo,
    msg: ExecuteMsg,
) -> Result<Response, ContractError> {
    use ExecuteMsg::*;

    match msg {
        AddMembers { admins } => exec::add_members(deps, info, admins),
        Leave {} => exec::leave(deps, info).map_err(Into::into),
        Donate {} => exec::donate(deps, info),
    }
}

mod exec {
    use super::*;

    pub fn add_members(
        deps: DepsMut,
        info: MessageInfo,
        admins: Vec<String>,
    ) -> Result<Response, ContractError> {
        let mut curr_admins = ADMINS.load(deps.storage)?;
        if !curr_admins.contains(&info.sender) {
            return Err(ContractError::Unauthorized {
                sender: info.sender,
            });
        }

        let events = admins
            .iter()
            .map(|admin| Event::new("admin_added").add_attribute("addr", admin));
        let resp = Response::new()
            .add_events(events)
            .add_attribute("action", "add_members")
            .add_attribute("added_count", admins.len().to_string());

        let admins: StdResult<Vec<_>> = admins
            .into_iter()
            .map(|addr| deps.api.addr_validate(&addr))
            .collect();

        curr_admins.append(&mut admins?);
        ADMINS.save(deps.storage, &curr_admins)?;

        Ok(resp)
    }

    pub fn leave(deps: DepsMut, info: MessageInfo) -> StdResult<Response> {
        ADMINS.update(deps.storage, move |admins| -> StdResult<_> {
            let admins = admins
                .into_iter()
                .filter(|admin| *admin != info.sender)
                .collect();
            Ok(admins)
        })?;

        Ok(Response::new())
    }

    pub fn donate(deps: DepsMut, info: MessageInfo) -> Result<Response, ContractError> {
        let denom = DONATION_DENOM.load(deps.storage)?;
        let admins = ADMINS.load(deps.storage)?;

        let donation = cw_utils::must_pay(&info, &denom)?.u128();

        let donation_per_admin = donation / (admins.len() as u128);

        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)
    }
}

mod query {
    use super::*;

    pub fn greet() -> StdResult<GreetResp> {
        let resp = GreetResp {
            message: "Hello World".to_owned(),
        };

        Ok(resp)
    }

    pub fn admins_list(deps: Deps) -> StdResult<AdminsListResp> {
        let admins = ADMINS.load(deps.storage)?;
        let resp = AdminsListResp { admins };
        Ok(resp)
    }
}

#[cfg(test)]
mod tests {
    use cosmwasm_std::Addr;
    use cw_multi_test::{App, ContractWrapper, Executor};

    use crate::msg::AdminsListResp;

    use super::*;

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

        let code = ContractWrapper::new(execute, instantiate, query);
        let code_id = app.store_code(Box::new(code));

        let addr = app
            .instantiate_contract(
                code_id,
                Addr::unchecked("owner"),
                &InstantiateMsg {
                    admins: vec![],
                    donation_denom: "eth".to_owned(),
                },
                &[],
                "Contract",
                None,
            )
            .unwrap();

        let resp: AdminsListResp = app
            .wrap()
            .query_wasm_smart(addr, &QueryMsg::AdminsList {})
            .unwrap();

        assert_eq!(resp, AdminsListResp { admins: vec![] });

        let addr = app
            .instantiate_contract(
                code_id,
                Addr::unchecked("owner"),
                &InstantiateMsg {
                    admins: vec!["admin1".to_owned(), "admin2".to_owned()],
                    donation_denom: "eth".to_owned(),
                },
                &[],
                "Contract 2",
                None,
            )
            .unwrap();

        let resp: AdminsListResp = app
            .wrap()
            .query_wasm_smart(addr, &QueryMsg::AdminsList {})
            .unwrap();

        assert_eq!(
            resp,
            AdminsListResp {
                admins: vec![Addr::unchecked("admin1"), Addr::unchecked("admin2")],
            }
        );
    }

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

        let code = ContractWrapper::new(execute, instantiate, query);
        let code_id = app.store_code(Box::new(code));

        let addr = app
            .instantiate_contract(
                code_id,
                Addr::unchecked("owner"),
                &InstantiateMsg {
                    admins: vec![],
                    donation_denom: "eth".to_owned(),
                },
                &[],
                "Contract",
                None,
            )
            .unwrap();

        let resp: GreetResp = app
            .wrap()
            .query_wasm_smart(addr, &QueryMsg::Greet {})
            .unwrap();

        assert_eq!(
            resp,
            GreetResp {
                message: "Hello World".to_owned()
            }
        );
    }

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

        let code = ContractWrapper::new(execute, instantiate, query);
        let code_id = app.store_code(Box::new(code));

        let addr = app
            .instantiate_contract(
                code_id,
                Addr::unchecked("owner"),
                &InstantiateMsg {
                    admins: vec![],
                    donation_denom: "eth".to_owned(),
                },
                &[],
                "Contract",
                None,
            )
            .unwrap();

        let err = app
            .execute_contract(
                Addr::unchecked("user"),
                addr,
                &ExecuteMsg::AddMembers {
                    admins: vec!["user".to_owned()],
                },
                &[],
            )
            .unwrap_err();

        assert_eq!(
            ContractError::Unauthorized {
                sender: Addr::unchecked("user")
            },
            err.downcast().unwrap()
        );
    }

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

        let code = ContractWrapper::new(execute, instantiate, query);
        let code_id = app.store_code(Box::new(code));

        let addr = app
            .instantiate_contract(
                code_id,
                Addr::unchecked("owner"),
                &InstantiateMsg {
                    admins: vec!["owner".to_owned()],
                    donation_denom: "eth".to_owned(),
                },
                &[],
                "Contract",
                None,
            )
            .unwrap();

        let resp = app
            .execute_contract(
                Addr::unchecked("owner"),
                addr,
                &ExecuteMsg::AddMembers {
                    admins: vec!["user".to_owned()],
                },
                &[],
            )
            .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_members"
        );
        assert_eq!(
            wasm.attributes
                .iter()
                .find(|attr| attr.key == "added_count")
                .unwrap()
                .value,
            "1"
        );

        let admin_added: Vec<_> = resp
            .events
            .iter()
            .filter(|ev| ev.ty == "wasm-admin_added")
            .collect();
        assert_eq!(admin_added.len(), 1);

        assert_eq!(
            admin_added[0]
                .attributes
                .iter()
                .find(|attr| attr.key == "addr")
                .unwrap()
                .value,
            "user"
        );
    }
}

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 cw_utils::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}")]
    StdError(#[from] StdError),
    #[error("{sender} is not 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.

use crate::error::ContractError;
use crate::msg::{AdminsListResp, ExecuteMsg, GreetResp, InstantiateMsg, QueryMsg};
use crate::state::{ADMINS, DONATION_DENOM};
use cosmwasm_std::{
    coins, to_binary, BankMsg, Binary, Deps, DepsMut, Env, Event, MessageInfo, Response, StdResult,
};

pub fn instantiate(
    deps: DepsMut,
    _env: Env,
    _info: MessageInfo,
    msg: InstantiateMsg,
) -> StdResult<Response> {
    let admins: StdResult<Vec<_>> = msg
        .admins
        .into_iter()
        .map(|addr| deps.api.addr_validate(&addr))
        .collect();
    ADMINS.save(deps.storage, &admins?)?;
    DONATION_DENOM.save(deps.storage, &msg.donation_denom)?;

    Ok(Response::new())
}

pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
    use QueryMsg::*;

    match msg {
        Greet {} => to_binary(&query::greet()?),
        AdminsList {} => to_binary(&query::admins_list(deps)?),
    }
}

pub fn execute(
    deps: DepsMut,
    _env: Env,
    info: MessageInfo,
    msg: ExecuteMsg,
) -> Result<Response, ContractError> {
    use ExecuteMsg::*;

    match msg {
        AddMembers { admins } => exec::add_members(deps, info, admins),
        Leave {} => exec::leave(deps, info).map_err(Into::into),
        Donate {} => exec::donate(deps, info),
    }
}

mod exec {
    use super::*;

    pub fn add_members(
        deps: DepsMut,
        info: MessageInfo,
        admins: Vec<String>,
    ) -> Result<Response, ContractError> {
        let mut curr_admins = ADMINS.load(deps.storage)?;
        if !curr_admins.contains(&info.sender) {
            return Err(ContractError::Unauthorized {
                sender: info.sender,
            });
        }

        let events = admins
            .iter()
            .map(|admin| Event::new("admin_added").add_attribute("addr", admin));
        let resp = Response::new()
            .add_events(events)
            .add_attribute("action", "add_members")
            .add_attribute("added_count", admins.len().to_string());

        let admins: StdResult<Vec<_>> = admins
            .into_iter()
            .map(|addr| deps.api.addr_validate(&addr))
            .collect();

        curr_admins.append(&mut admins?);
        ADMINS.save(deps.storage, &curr_admins)?;

        Ok(resp)
    }

    pub fn leave(deps: DepsMut, info: MessageInfo) -> StdResult<Response> {
        ADMINS.update(deps.storage, move |admins| -> StdResult<_> {
            let admins = admins
                .into_iter()
                .filter(|admin| *admin != info.sender)
                .collect();
            Ok(admins)
        })?;

        Ok(Response::new())
    }

    pub fn donate(deps: DepsMut, info: MessageInfo) -> Result<Response, ContractError> {
        let denom = DONATION_DENOM.load(deps.storage)?;
        let admins = ADMINS.load(deps.storage)?;

        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 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)
    }
}

mod query {
    use super::*;

    pub fn greet() -> StdResult<GreetResp> {
        let resp = GreetResp {
            message: "Hello World".to_owned(),
        };

        Ok(resp)
    }

    pub fn admins_list(deps: Deps) -> StdResult<AdminsListResp> {
        let admins = ADMINS.load(deps.storage)?;
        let resp = AdminsListResp { admins };
        Ok(resp)
    }
}

#[cfg(test)]
mod tests {
    use cosmwasm_std::Addr;
    use cw_multi_test::{App, ContractWrapper, Executor};

    use crate::msg::AdminsListResp;

    use super::*;

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

        let code = ContractWrapper::new(execute, instantiate, query);
        let code_id = app.store_code(Box::new(code));

        let addr = app
            .instantiate_contract(
                code_id,
                Addr::unchecked("owner"),
                &InstantiateMsg {
                    admins: vec![],
                    donation_denom: "eth".to_owned(),
                },
                &[],
                "Contract",
                None,
            )
            .unwrap();

        let resp: AdminsListResp = app
            .wrap()
            .query_wasm_smart(addr, &QueryMsg::AdminsList {})
            .unwrap();

        assert_eq!(resp, AdminsListResp { admins: vec![] });

        let addr = app
            .instantiate_contract(
                code_id,
                Addr::unchecked("owner"),
                &InstantiateMsg {
                    admins: vec!["admin1".to_owned(), "admin2".to_owned()],
                    donation_denom: "eth".to_owned(),
                },
                &[],
                "Contract 2",
                None,
            )
            .unwrap();

        let resp: AdminsListResp = app
            .wrap()
            .query_wasm_smart(addr, &QueryMsg::AdminsList {})
            .unwrap();

        assert_eq!(
            resp,
            AdminsListResp {
                admins: vec![Addr::unchecked("admin1"), Addr::unchecked("admin2")],
            }
        );
    }

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

        let code = ContractWrapper::new(execute, instantiate, query);
        let code_id = app.store_code(Box::new(code));

        let addr = app
            .instantiate_contract(
                code_id,
                Addr::unchecked("owner"),
                &InstantiateMsg {
                    admins: vec![],
                    donation_denom: "eth".to_owned(),
                },
                &[],
                "Contract",
                None,
            )
            .unwrap();

        let resp: GreetResp = app
            .wrap()
            .query_wasm_smart(addr, &QueryMsg::Greet {})
            .unwrap();

        assert_eq!(
            resp,
            GreetResp {
                message: "Hello World".to_owned()
            }
        );
    }

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

        let code = ContractWrapper::new(execute, instantiate, query);
        let code_id = app.store_code(Box::new(code));

        let addr = app
            .instantiate_contract(
                code_id,
                Addr::unchecked("owner"),
                &InstantiateMsg {
                    admins: vec![],
                    donation_denom: "eth".to_owned(),
                },
                &[],
                "Contract",
                None,
            )
            .unwrap();

        let err = app
            .execute_contract(
                Addr::unchecked("user"),
                addr,
                &ExecuteMsg::AddMembers {
                    admins: vec!["user".to_owned()],
                },
                &[],
            )
            .unwrap_err();

        assert_eq!(
            ContractError::Unauthorized {
                sender: Addr::unchecked("user")
            },
            err.downcast().unwrap()
        );
    }

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

        let code = ContractWrapper::new(execute, instantiate, query);
        let code_id = app.store_code(Box::new(code));

        let addr = app
            .instantiate_contract(
                code_id,
                Addr::unchecked("owner"),
                &InstantiateMsg {
                    admins: vec!["owner".to_owned()],
                    donation_denom: "eth".to_owned(),
                },
                &[],
                "Contract",
                None,
            )
            .unwrap();

        let resp = app
            .execute_contract(
                Addr::unchecked("owner"),
                addr,
                &ExecuteMsg::AddMembers {
                    admins: vec!["user".to_owned()],
                },
                &[],
            )
            .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_members"
        );
        assert_eq!(
            wasm.attributes
                .iter()
                .find(|attr| attr.key == "added_count")
                .unwrap()
                .value,
            "1"
        );

        let admin_added: Vec<_> = resp
            .events
            .iter()
            .filter(|ev| ev.ty == "wasm-admin_added")
            .collect();
        assert_eq!(admin_added.len(), 1);

        assert_eq!(
            admin_added[0]
                .attributes
                .iter()
                .find(|attr| attr.key == "addr")
                .unwrap()
                .value,
            "user"
        );
    }

    #[test]
    fn donations() {
        let mut app = App::new(|router, _, storage| {
            router
                .bank
                .init_balance(storage, &Addr::unchecked("user"), coins(5, "eth"))
                .unwrap()
        });

        let code = ContractWrapper::new(execute, instantiate, query);
        let code_id = app.store_code(Box::new(code));

        let addr = app
            .instantiate_contract(
                code_id,
                Addr::unchecked("owner"),
                &InstantiateMsg {
                    admins: vec!["admin1".to_owned(), "admin2".to_owned()],
                    donation_denom: "eth".to_owned(),
                },
                &[],
                "Contract",
                None,
            )
            .unwrap();

        app.execute_contract(
            Addr::unchecked("user"),
            addr.clone(),
            &ExecuteMsg::Donate {},
            &coins(5, "eth"),
        )
        .unwrap();

        assert_eq!(
            app.wrap()
                .query_balance("user", "eth")
                .unwrap()
                .amount
                .u128(),
            0
        );

        assert_eq!(
            app.wrap()
                .query_balance(&addr, "eth")
                .unwrap()
                .amount
                .u128(),
            1
        );

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

        assert_eq!(
            app.wrap()
                .query_balance("admin2", "eth")
                .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 a user 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 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 nevermore 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 serde using those variant names. The problem is that it is more common to use snake cases for field names in the JSON world. Hopefully, there is an effortless way to tell serde, to change the names casing for serialization purposes. Let's update our messages with a #[serde] attribute:

use cosmwasm_std::Addr;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
#[serde(rename_all = "snake_case")]
pub struct InstantiateMsg {
    pub admins: Vec<String>,
    pub donation_denom: String,
}

#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
#[serde(rename_all = "snake_case")]
pub enum ExecuteMsg {
    AddMembers { admins: Vec<String> },
    Leave {},
    Donate {},
}

#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
#[serde(rename_all = "snake_case")]
pub struct GreetResp {
    pub message: String,
}

#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
#[serde(rename_all = "snake_case")]
pub struct AdminsListResp {
    pub admins: Vec<Addr>,
}

#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
#[serde(rename_all = "snake_case")]
pub enum QueryMsg {
    Greet {},
    AdminsList {},
}

JSON schema

Talking about JSON API, it is worth mentioning JSON Schema. It is a way of defining a shape for JSON messages. It is 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. Go to the Cargo.toml:

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

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

[dependencies]
cosmwasm-std = { version = "1.1.4", features = ["staking"] }
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
cw-storage-plus = "0.15.1"
thiserror = "1"
schemars = "0.8.1"
cosmwasm-schema = "1.1.4"

[dev-dependencies]
cw-multi-test = "0.13.4"

There is one additional change in this file - in crate-type I added "rlib". "cdylib" crates cannot be used as typical Rust dependencies. As a consequence, it is impossible to create examples for such crates.

Now go back to src/msg.rs and add a new derive for all messages:

use cosmwasm_std::Addr;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

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

#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ExecuteMsg {
    AddMembers { admins: Vec<String> },
    Leave {},
    Donate {},
}

#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct GreetResp {
    pub message: String,
}

#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct AdminsListResp {
    pub admins: Vec<Addr>,
}

#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum QueryMsg {
    Greet {},
    AdminsList {},
}

You may argue that all those derives look slightly clunky, and I agree. Hopefully, the cosmwasm-schema crate delivers a utility cw_serde macro, which we can use to reduce a boilerplate:

use cosmwasm_std::Addr;
use cosmwasm_schema::cw_serde

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

#[cw_serde]
pub enum ExecuteMsg {
    AddMembers { admins: Vec<String> },
    Leave {},
    Donate {},
}

#[cw_serde]
pub struct GreetResp {
    pub message: String,
}

#[cw_serde]
pub struct AdminsListResp {
    pub admins: Vec<Addr>,
}

#[cw_serde]
pub enum QueryMsg {
    Greet {},
    AdminsList {},
}

Additionally, we have to derive the additional QueryResponses trait for our query message to correlate the message variants with responses we would generate for them:

use cosmwasm_std::Addr;
use cosmwasm_schema::{cw_serde, QueryResponses}

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

#[cw_serde]
pub enum ExecuteMsg {
    AddMembers { admins: Vec<String> },
    Leave {},
    Donate {},
}

#[cw_serde]
pub struct GreetResp {
    pub message: String,
}

#[cw_serde]
pub struct AdminsListResp {
    pub admins: Vec<Addr>,
}

#[cw_serde]
#[derive(QueryResponses)]
pub enum QueryMsg {
    #[returns(GreetResp)]
    Greet {},
    #[returns(AdminsListResp)]
    AdminsList {},
}

The QueryResponses is a trait that requires the #[returns(...)] attribute to all your query variants to generate additional information about the query-response relationship.

Now, we want to make the msg module public and accessible by crates depending on our contract (in this case - for schema example). Update a src/lib.rs:

use cosmwasm_std::{entry_point, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult};
use error::ContractError;
use msg::{ExecuteMsg, InstantiateMsg, QueryMsg};

pub mod contract;
pub mod error;
pub mod msg;
pub mod state;

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

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

#[entry_point]
pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
    contract::query(deps, env, msg)
}

I changed the visibility of all modules - as our crate can now be used as a dependency. If someone would like to do so, he may need access to handlers or state.

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

use contract::msg::{ExecuteMsg, InstantiateMsg, QueryMsg};
use cosmwasm_schema::write_api;

fn main() {
    write_api! {
        instantiate: InstantiateMsg,
        execute: ExecuteMsg,
        query: QueryMsg
    }
}

Cargo is smart enough to recognize files in 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.52s
     Running `target/debug/schema schema`
Removing "/home/hashed/confio/git/book/examples/03-basics/schema/contract.json" …
Exported the full API as /home/hashed/confio/git/book/examples/03-basics/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, in the end, the most important one. 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 so that one can generate schema calling simply cargo schema.

Disabling entry points for libraries

Since we added the "rlib" target for the contract, it is, as mentioned before, useable as a dependency. The problem is that the contract depended 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:

[features]
library = []

This way, we created a new feature for our crate. Now we want to disable the entry_point attribute on entry points - we will do it by a slight update of src/lib.rs:

use cosmwasm_std::{entry_point, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult};
use error::ContractError;
use msg::{ExecuteMsg, InstantiateMsg, QueryMsg};

pub mod contract;
pub mod error;
pub mod msg;
pub mod state;

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    msg: InstantiateMsg,
) -> StdResult<Response> {
    contract::instantiate(deps, env, info, msg)
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    msg: ExecuteMsg,
) -> Result<Response, ContractError> {
    contract::execute(deps, env, info, msg)
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
    contract::query(deps, env, msg)
}

The cfg_attr attribute 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 check_contract. It is implemented as an example on cosmwasm vm, but I encourage you to build it and put the binary available from your PATH so you can use it easily.

This limitation has two implications. First, you always have to use decimal 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.

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.

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.

Cross contract communication

We already covered creating a single isolating contract. However, SOLID principles tell us that entities should be as small as reasonably possible - such as they have a single responsibility. Entities we are focusing on now are smart contracts, and we want to make sure that every smart contract has a sole responsibility it takes care of.

But we also want to build complex systems using smart contracts. To do so, we need to be able to communicate between them. We already talked about what such communication looks like using an actor model. Now it's time to use this knowledge in practice.

In this chapter, we will improve the previously created administration group model to solve the problem I brought - the possibility of adding own multiple addresses by a single admin to take bigger donation parts.

We would also give admins some work to do besides being admins.

Design

This time we will start discussing the design of our system a bit. Building multi-contract systems tend to be a bit more complicated than just isolated contracts, so I want to give you some anchor on what we are building in this chapter. If you feel lost with a design, don't worry - it will get clear while implementing contracts. For now, go through it to get a general idea.

First, let's think about the problem we want to solve. Our admins are a vector of addresses. Anyone already an admin can add anyone he wants to the list. But this "anyone" can be a second instance of the same admin account, so he counts twice for donations!

This issue is relatively simple to fix, but there is another problem - as we already learned, the admin could create a smart contract which he and only he can withdraw tokens from and register as another admin in the group! Instantiating it multiple times, he can achieve his goal even if we prevent adding the same address multiple times. There would be many distinct addresses that the same person owns.

It looks like an unpleasant situation, but there are ways to manage it. The one we would implement is voting. Instead of being able to add another admin to the list, admins would be allowed to propose their colleagues as new admins. It would start a voting process - everyone who was an admin at the time of the proposal creation would be able to support it. If more than half admins would support the new candidate, he would immediately become an admin.

It is not the most convoluted voting process, but it would be enough for our purposes.

Voting process

To achieve this goal, we would create two smart contracts. First, one would be reused contract from the Basics chapter - it would be an admin contract. Additionally, we would add a voting contract. It would be responsible for managing a single voting process. It would be instantiated by an admin contract whenever an admin wants to add his friend to a list. Here is a diagram of the contracts relationship:

Here is adding an admin flowchart - assuming there are 5 admins on the contract already, but 2 of them did nothing:

I already put some hints about contracts implementation, but I will not go into them yet.

Messages forwarding

There is one other thing we want to add - some way to give admins work. The admin contract would behave like a proxy to call another contract. That means that some other external contract would just set our admin instance as a specific address that can perform executions on it, and admins would perform actions this way. The external contract would see execution as the admin contract would do it. Here is an updated contracts diagram:

And calling external contract flowchart:

Note that the msg on execute admin contract message is some arbitrary message just forwarded to the external contract. It would be a base64-encoded message in the real world, but it is just an implementation detail.

Ultimately, we will create a simple example of an external contract to understand how to use such a pattern.

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