Introduction to cosmwasm
Last updated
Last updated
Let's examine what a smart contract is and the way it works under the hood. The following is a minimal contract that stores a counter value which can be incremented and everyone can query this counter value.
The counter contract template can be found in .
A smart contract can be considered an instance of a singleton object whose internal state is persisted on the blockchain. Users can trigger state changes by sending the smart contract JSON messages, and users can also query its state by sending a request formatted as a JSON message.
As a smart contract writer, your job is to define 3 functions that compose your smart contract's interface:
instantiate()
: a constructor which is called during contract instantiation to provide the initial state
execute()
: gets called when a user wants to invoke a method on the smart contract, this invokes one of the execute messages defined in the contract under ExecuteMsg
enum which is essentially a list of transactions the contract supports
query()
: gets called when a user wants to get data out of a smart contract, this invokes one of the query messages defined in the contract under QueryMsg
enum which is a list of queries the contract supports
In our sample counter contract, we will implement one instantiate, one query, and two execute methods.
Following is the file structure of the contract source:
Cargo.toml
: Has contract's rust dependencies and some configuration parameters.
src/contract.rs
: Contract entry points such as instantiate()
, execute()
and query()
are defined here.
src/error.rs
: The contract's custom error messages are defined here.
src/lib.rs
: This file defines the list of rust source files that are part of the contract, add the file name here when adding a new one to the contract's source.
src/msg.rs
: Defines all the messages allowed to communicate with the contract.
src/state.rs
: Defines what the structure of the contract's storage will be.
src/bin/schema.rs
: Defines how to generate schema (language-agnostic format of contract messages) from the contract.
State
handles the state of the database where smart contract data is stored and accessed.
The counter contract has the following basic state, a singleton struct State
containing:
count
, a 32-bit integer with which execute()
messages will interact by increasing or resetting it.
owner
, the sender address
of the MsgInstantiateContract
, which will determine if certain execution messages are permitted.
Notice how the State
struct holds both count
and owner
. In addition, the derive attribute is applied to auto-implement some useful traits:
Serialize
: provides serialization
Deserialize
: provides deserialization
Clone
: makes the struct copyable
Debug
: enables the struct to be printed to string
PartialEq
: provides equality comparison
JsonSchema
: auto-generates a JSON schema
Addr
refers to a human-readable Empe address prefixed with empe, e.g. empe1zqemr4tmnxvq9jjxchqggmjz2ddkvgqujc33hk
.
The InstantiateMsg
is provided to the contract when a user instantiates a contract on the blockchain through a MsgInstantiateContract
. This provides the contract with its configuration as well as its initial state.
The contract creator is expected to supply the initial state in a JSON message. We can see in the message definition below that the message holds one parameter count, which represents the initial count.
The ExecuteMsg
is a JSON message passed to the execute()
function through a MsgExecuteContract
. Unlike the InstantiateMsg
, the ExecuteMsg
can exist as several different types of messages to account for the different types of functions that a smart contract can expose to a user. The execute()
function demultiplexes these different types of messages to its appropriate message handler logic.
We have two ExecuteMsg
:
Increment
has no input parameter and increases the value of count by 1.
Reset
takes a 32-bit integer as a parameter and resets the value of count
to the input parameter.
Any user can increment the current count by 1 using the following message:
Only the owner can reset the count to a specific number. See Logic below for the implementation details.
To support data queries in the contract, you'll have to define both a QueryMsg
format (which represents requests), as well as provide the structure of the query's output, CountResponse
in this case. You must do this because query()
will send information back to the user through structured JSON, so you must make the shape of your response known.
Fetch count from the contract using the following message:
Which should return:
We've now defined the contract storage, messages the contract can receive.
We now need to specify what method will be called for a given message the contract receives. In other words, what action will be taken when a certain message is received by the contract, which can optionally include updating the state of contract.
Entry point defines how an external client can interact with the contract deployed on the chain.
The following entry points are commonly seen in contracts:
In contract.rs
, you will define your first entry-point, instantiate()
, or where the contract is instantiated and passed its InstantiateMsg
. Extract the count from the message and set up your initial state where:
count
is assigned the count from the message
owner
is assigned to the sender of the MsgInstantiateContract
This is your execute()
method, which uses Rust's pattern matching to route the received ExecuteMsg to the appropriate handling logic, either dispatching a try_increment()
or a try_reset()
call depending on the message received.
In try_increment()
, it acquires a mutable reference to the storage to update the item located at the key state
. It then updates the state's count by returning an Ok
result with the new state. Finally, it terminates the contract's execution with an acknowledgment of success by returning an Ok
result with the Response
.
The logic for reset is very similar to increment, except this time, it first checks that the message sender is permitted to invoke the reset function (in this case, it must be the contract owner).
The logic for query()
is similar to that of execute()
; however, since query()
is called without the end-user making a transaction, the env
argument is omitted as no information is necessary.
Note that deps is of type Deps
, not DepsMut
(Mut
stands for mutable) as in the execute()
, which implies that queries are for read-only operations and do not make any changes to contract's storage.
Empe utilizes as the smart contract execution layer. In CosmWasm, the uploading of a contract's code and the instantiation of a contract are regarded as separate events, unlike on Ethereum. This is to allow a small set of vetted contract archetypes to exist as multiple instances sharing the same base code, but be configured with different parameters (imagine one canonical ERC20, and multiple tokens that use its code).