Contract state

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

Adding contract state

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

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

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

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

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

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

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

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

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

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

New types:

  • Map<_, _>

  • Addr - representation of actual address on a blockchain.

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

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

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

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

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

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

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

Initializing the state

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

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

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

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

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

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

        Ok(Response::new())
    }
}

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

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

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

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

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

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

pub mod contract;

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

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

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

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

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

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