CosmWasm for CTOs II: Advanced Usage

Ethan Frey
CosmWasm
Published in
6 min readNov 4, 2021

On top of secure foundations, we have built a powerful and flexible framework.

Photo by Alex Knight on Unsplash

Check out the intro to this series for context and links to all other articles.

There are a number of advanced use cases that I referenced in the last article, but which I couldn’t dig into. Here I will explain some of these more advanced use cases. And map out solutions to the problems you will encounter as the complexity of your project grows.

Contract Migration

No code is perfect on the first version, and Ethereum’s ideal of “immutable code” quickly hit the messy reality that “someone” had to be able to upgrade code in order to fix bugs and add features. Since this was not baked in from day one, there are some complex designs using library contract and proxy contracts that allowed some owner to change the code that ran when you called the same address, while maintaining the same state. However, a number of bugs (including the infamous Parity Multisig self-destruct) were due to minor issues in such designs.

With the benefit of hindsight, CosmWasm made contract migration a first-class experience. When instantiating a contract, there is an optional admin field that you can set. If it is left empty, the contract is immutable. If it is set (to an external account or governance contract), that account can trigger a migration. The admin can also change admin or even make the contract fully immutable after some time.

Performing a contract migration is a three step process. First, you must write a newer version of the contract you wish to update. Second, you can upload the new code as you did before, but don’t instantiate it. Third, you use a dedicated MsgMigrateContract transaction to point this contract to use the new code. And you are done!

Sometimes there are state changes or extra fields that need to be initialized. And you want to make sure you are updating to a compatible contract. This is why we pass a msg field on the migration message, and we call a migrate function on the new code, to make any needed migrations. If the migrate function returns an error, the transaction aborts, all state changes are reverted and the migration is not performed. You can see how we added a basic migrate to the cw1-subkeys contract (allowing us to upgrade code with no state transitions). It boils down more or less to this:

You can also look at our simple Hackatom example, where we do self-migrations (contract to itself) and use the message to change some otherwise immutible state.

Storage Helpers

Before working on blockchain, I worked on “web technologies”, mainly backend API servers and databases. You never had to touch raw bytes, and always had some higher level language to modify state. First raw SQL, and then many ORM libraries that allow you to define structs and convert them to PostgreSQL tables or MongoDB collections.

Solidity caries on some of that idea and allows you to define simple types and lookups like mapping(address => uint). This is a far cry from SQL capabilities, but provides some ideas of higher order constructs. The Cosmos SDK provides more data access, like iterating over ranges, but in the end it just exposed raw bytes in the central KVStore abstraction. And there are no common abstractions to work with this.

CosmWasm also defines a similar Storage interface working with raw bytes that we use for the Wasm boundary. However, we do not want to force application developers to think about this and worked on helpful abstractions to maximize productivity. The first attempt was cosmwasm-storage, which provided Singleton and Bucket types and hid all the value serialization. Learning from the limits with this, we also built cw-storage-plus, which provides Item and Map as the common primitives.

The README file gives a great run-down of all the functionality, but I will highlight a few key points here. You can use many types as keys, even tuples, so you can define Map<(&Addr, U32Key), Vote> to store a vote by eg. voter address and proposal ID. Not only can you query VOTES.load(storage, (&sender, 5)) but you can also efficiently perform complex operations, like iterating on the second parts of composite keys, to eg. get the 10 most recent votes by a given address: VOTES.prefix(&sender).range(storage, None, None, Order::Descending).limit(10). No need to spend hours writing custom helpers for this logic, it is provided out-of-the-box with a clean API.

We also provide a way to automatically store and query secondary indexed on a Map, using an IndexedMap. This is a bit more work to configure (as you need to define the indexer as a function and compose them), but it will automatically track the secondary indexes for all items and update as needed when you do normal save. You can also enforce a uniqueness constraint in one line, and obviously perform queries on the secondary index. This requires an extra storage write per secondary index for each update to keep them in sync, but for item that are not updated often, it can provide powerful query capabilities without writing any of this complex storage logic yourself.

We also provide SnapshotMaps, which can lazily snapshot the state at any checkpoint, allowing historical queries. We use this, for example, in the cw4-group contract so that a voting contract can use a snapshot of weights at the opening of the proposal, without ever having to copy all weights.

cw-storage-plus contains a ton of useful helpers and the best way to learn it may be to simply read state.rs on various cw-plus contracts and see what types they use. It is meant to have a simple-to-use API once you see it in action. I think this is a huge productivity advantage and one reason I can no longer go back to developing in the Cosmos SDK.

Submessages Explained

In the last article, I mentioned that the default handling of messages returned by the contract is to revert on error, but that one could use Submessages to handle the return value. This can cover any case when we want to handle the result of executing another contract, but I will cover the two main ones.

The first case, is to instantiate another contract and then get the address that was just created. In fact, this was the primary motivator of adding some “callback feature” to CosmWasm. When a contract is instantiated, x/wasm emits a standard event instantiate._contract_addr that contains the address of the newly emitted contract. It also emits a protobuf encoded message in the data field, containing the new address and the data returned from the contract.

This is used in Tgrade’s TFi AMM Factory, when it creates a new trading pair and needs to determine the address of the contract it just created. Given this is such a common idiom, we recently added some helpers to cw-plus, to allow you to parse the reply from WasmMsg::Instantiate, as well as the data returned from WasmMsg::Execute.

The second case is to allow a message to fail, without the main message failing. In IBC, for example, if the packet is rejected, the handler should not return an error (which would revert the whole transaction), but rather return acknowledgement data with a serialized error packet, that can be relayed to the other side and handled there. When building cw20-icw20, we had to handle the case where our code made a potentially failing Cw20::Transfer message, but needed to ensure the transaction didn’t fail.

We do this by executing the sendAmount action as a SubMsg. We use SubMsg::reply_on_error to ensure we get a callback if the message fails. And in the reply handler, we handle the error case by writing a new data field with a failed ack packet to be relayed to the original sender. But notably, do not return an error on the transaction. The state of the submessage (cw20 contract) will be reverted, but the main message (and any state changes in IBC or cw20-ics20) will not be reverted.

The above gist captures the main flows I linked to… using SubMsg::reply_on_error to wrap the returned message, then handling that given reply.id in the reply entry point, which is guaranteed to be called if it ever errors. (And has no overhead on success cases, as it is not even called)

Next Steps

Interested? Ready to apply these advanced features to enhance your application architecture? Start your CosmWasm project today starting with our tutorials and docs, or join our CosmWasm chat for more info.

You’ll be in good company.

--

--