Map storage

There is one thing to be immediately improved in the admin contract. Let's check the contract state:


#![allow(unused)]
fn main() {
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");
}

Note that we keep our admin list as a single vector. However, in the whole contract, in most cases, we access only a single element of this vector.

This is not ideal, as now, whenever we want to access the single admin entry, we have first to deserialize the list containing all of them and then iterate over them until we find the interesting one. This might consume a serious amount of gas and is completely unnecessary overhead - we can avoid that using the Map storage accessor.

The Map storage

First, let's define a map - in this context, it would be a set of keys with values assigned to them, just like a HashMap in Rust or dictionaries in many languages. We define it as similar to an Item, but this time we need two types - the key type and the value type:


#![allow(unused)]
fn main() {
use cw_storage_plus::Map;

pub const STR_TO_INT_MAP: Map<String, u64> = Map::new("str_to_int_map");
}

Then to store some items on the Map, we use a save method - same as for an Item:


#![allow(unused)]
fn main() {
STR_TO_INT_MAP.save(deps.storage, "ten".to_owned(), 10);
STR_TO_INT_MAP.save(deps.storage, "one".to_owned(), 1);
}

Accessing entries in the map is also as easy as reading an item:


#![allow(unused)]
fn main() {
let ten = STR_TO_INT_MAP.load(deps.storage, "ten".to_owned())?;
assert_eq!(ten, 10);

let two = STR_TO_INT_MAP.may_load(deps.storage, "two".to_owned())?;
assert_eq!(two, None);
}

Obviously, if the element is missing in the map, the load function will result in an error - just like for an item. On the other hand - may_load returns a Some variant when element exits.

Another very useful accessor that is specific to the map is the has function, which checks for the existence of the key in the map:


#![allow(unused)]
fn main() {
let contains = STR_TO_INT_MAP.has(deps.storage, "three".to_owned())?;
assert!(!contains);
}

Finally, we can iterate over elements of the maps - either its keys or key-value pairs:


#![allow(unused)]
fn main() {
use cosmwasm_std::Order;

for k in STR_TO_INT_MAP.keys(deps.storage, None, None, Order::Ascending) {
    let _addr = deps.api.addr_validate(k?);
}

for item in STR_TO_INT_MAP.range(deps.storage, None, None, Order::Ascending) {
    let (_key, _value) = item?;
}
}

First, you might wonder about extra values passed to keys and range - those are in order: lower and higher bounds of iterated elements, and the order elements should be traversed.

While working with typical Rust iterators, you would probably first create an iterator over all the elements and then somehow skip those you are not interested in. After that, you will stop after the last interesting element.

It would more often than not require accessing elements you filter out, and this is the problem - it requires reading the element from the storage. And reading it from the storage is the expensive part of working with data, which we try to avoid as much as possible. One way to do it is to instruct the Map where to start and stop deserializing elements from storage so it never reaches those outside the range.

Another critical thing to notice is that the iterator returned by both keys and range functions are not iterators over elements - they are iterators over Results. It is a thing because, as it is rare, it might be that item is supposed to exist, but there is some error while reading from storage - maybe the stored value is serialized in a way we didn't expect, and deserialization fails. This is actually a real thing that happened in one of the contracts I worked on in the past - we changed the value type of the Map, and then forgot to migrate it, which caused all sorts of problems.

Maps as sets

So I imagine you can call me crazy right now - why do I spam about a Map, while we are working with vector? It is clear that those two represent two distinct things! Or do they?

Let's reconsider what we keep in the ADMINS vector - we have a list of objects which we expect to be unique, which is a definition of a mathematical set. So now let me bring back my initial definition of the map:

First, let's define a map - in this context, it would be a set of keys with values assigned to them, just like a HashMap in Rust or dictionaries in many languages.

I purposely used the word "set" here - the map has the set built into it. It is a generalization of a set or reversing the logic - the set is a particular case of a map. If you imagine a set that map every single key to the same value, then the values become irrelevant, and such a map becomes a set semantically.

How can you make a map mapping all the keys to the same value? We pick a type with a single value. Typically in Rust, it would be a unit type (()), but in CosmWasm, we tend to use the Empty type from CW standard crate:


#![allow(unused)]
fn main() {
use cosmwasm_std::{Addr, Empty};
use cw_storage_plus::Map;

pub const ADMINS: Map<Addr, Empty> = Map::new("admins");
}

We now need to fix the usage of the map in our contract. Let's start with contract instantiation:


#![allow(unused)]
fn main() {
use crate::msg::InstantiateMsg;
use crate::state::{ADMINS, DONATION_DENOM};
use cosmwasm_std::{
    DepsMut, Empty, Env, MessageInfo, Response, StdResult,
};

pub fn instantiate(
    deps: DepsMut,
    _env: Env,
    _info: MessageInfo,
    msg: InstantiateMsg,
) -> StdResult<Response> {
    for addr in msg.admins {
        let admin = deps.api.addr_validate(&addr)?;
        ADMINS.save(deps.storage, admin, &Empty {})?;
    }
    DONATION_DENOM.save(deps.storage, &msg.donation_denom)?;

    Ok(Response::new())
}
}

It didn't simplify much, but we no longer need to collect our address. Then let's move to the leaving logic:


#![allow(unused)]
fn main() {
use crate::state::ADMINS;
use cosmwasm_std::{DepsMut, MessageInfo};

pub fn leave(deps: DepsMut, info: MessageInfo) -> StdResult<Response> {
    ADMINS.remove(deps.storage, info.sender.clone());

    let resp = Response::new()
        .add_attribute("action", "leave")
        .add_attribute("sender", info.sender.as_str());

    Ok(resp)
}
}

Here we see a difference - we don't need to load a whole vector. We remove a single entry with the remove function.

What I didn't emphasize before, and what is relevant, is that Map stores every single key as a distinct item. This way, accessing a single element will be cheaper than using a vector.

However, this has its downside - accessing all the elements is more gas-consuming using Map! In general, we tend to avoid such situations - the linear complexity of the contract might lead to very expensive executions (gas-wise) and potential vulnerabilities - if the user finds a way to create many dummy elements in such a vector, he may make the execution cost exceeding any gas limit.

Unfortunately, we have such an iteration in our contract - the distribution flow becomes as follows:


#![allow(unused)]
fn main() {
use crate::error::ContractError;
use crate::state::{ADMINS, DONATION_DENOM};
use cosmwasm_std::{
    coins, BankMsg,DepsMut, MessageInfo, Order, Response
};

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

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

If I had to write a contract like this, and this donate would be a critical, often called flow, I would advocate for going for an Item<Vec<Addr>> here. Fortunately, it is not the case - the distribution does not have to be linear in complexity! It might sound a bit crazy, as we have to iterate over all receivers to distribute funds, but this is not true - there is a pretty nice way to do so in constant time, which I will describe later in the book. For now, we will leave it as it is, acknowledging the flaw of the contract, which we will fix later.

The final function to fix is the admins_list query handler:


#![allow(unused)]
fn main() {
use crate::state::ADMINS;
use cosmwasm_std::{Deps, Order, StdResult};

pub fn admins_list(deps: Deps) -> StdResult<AdminsListResp> {
    let admins: Result<Vec<_>, _> = ADMINS
        .keys(deps.storage, None, None, Order::Ascending)
        .collect();
    let admins = admins?;
    let resp = AdminsListResp { admins };
    Ok(resp)
}
}

Here we also have an issue with linear complexity, but it is far less of a problem.

First, queries are often purposed to be called on local nodes, with no gas cost - we can query contracts as much as we want.

And then, even if we have some limit on execution time/cost, there is no reason to query all the items every single time! We will fix this function later, adding pagination - to limit the execution time/cost of the query caller would be able to ask for a limited amount of items starting from the given one. Knowing this chapter, you can probably figure implementation of it right now, but I will show the common way we do that when I go through common CosmWasm practices.

Reference keys

There is one subtlety to improve in our map usage.

The thing is that right now, we index the map with the owned Addr key. That forces us to clone it if we want to reuse the key (particularly in the leave implementation). This is not a huge cost, but we can avoid it - we can define the key of the map to be a reference:


#![allow(unused)]
fn main() {
use cosmwasm_std::{Addr, Empty};
use cw_storage_plus::Map;

pub const ADMINS: Map<&Addr, Empty> = Map::new("admins");
pub const DONATION_DENOM: Item<String> = Item::new("donation_denom");
}

Finally, we need to fix the usages of the map in two places:


#![allow(unused)]
fn main() {
use crate::state::{ADMINS, DONATION_DENOM};
use cosmwasm_std::{
    DepsMut, Empty, Env, MessageInfo, Response, StdResult,
};

pub fn instantiate(
    deps: DepsMut,
    _env: Env,
    _info: MessageInfo,
    msg: InstantiateMsg,
) -> StdResult<Response> {
    for addr in msg.admins {
        let admin = deps.api.addr_validate(&addr)?;
        ADMINS.save(deps.storage, &admin, &Empty {})?;
    }

    // ...

   DONATION_DENOM.save(deps.storage, &msg.donation_denom)?;

   Ok(Response::new())
}

pub fn leave(deps: DepsMut, info: MessageInfo) -> StdResult<Response> {
    ADMINS.remove(deps.storage, &info.sender);

    // ...

   let resp = Response::new()
       .add_attribute("action", "leave")
       .add_attribute("sender", info.sender.as_str());

   Ok(resp)
}
}