Terra smart contracts (cosmwasm)

Terra smart contracts are based on cosmwasm. However, Terra is not exactly up to date and compatible with the latest cosmwasm smart contracts and may not be able to compile and run on Terra's virtual machine.

Design of cosmwasm smart contracts

The architecture of cosmwasm smart contracts is very different from Solidity. Solidity is extremely easy to use and we can treat it without any considerations of blockchain architecture. To put it simply, the style of Solidity is OOP-style. We can adopt the same mindset of coding any object oriented language and treat smart contracts as classes/objects.

While there are indeed more advanced ways to structure your Solidity smart contracts, we will not discuss it as normal people don't do that anyways.

pragma solidity ^0.6.0;

interface IERC20 {
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function allowance(address owner, address spender) external view returns (uint256);

    function transfer(address recipient, uint256 amount) external returns (bool);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);


    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);
}

cosmwasm is not. cosmwasm contract is split into 4 portions:

  1. State (variables that you want to persist are defined here)

  2. Query (query the state (variables) of the smart contract)

  3. Execute (modify the state (variables) of the smart contract)

  4. Instantiate (to initialized the smart contract) This leads to some inconvenience while coding, but also more flexibility & upgradeability of the smart contract. For example, developers need to manually call & access the state, unlike in Solidity where we can access it like an object.

pragma solidity ^0.6.0;

contract Example {
    uint256 amount;
    
    function getAmount() returns (uint256) {
        return amount;
    }
    ...
}

Solidity code

// Declare amount in state.rs
pub const AMOUNT: Item<i32> = Item::new("amount");

// Reading amount in query.rs
use crate::state::{AMOUNT}

pub fn get_amount(deps: DepsMut) --> StdResult<i32> {
    CONFIG.load(deps.storage)?
}

// Declare wrapper for query's logic in contract.rs
// Recall contract is split into state, execute, query, instantiate
use ...

pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
    match msg {
        QueryMsg::GetAmount {} => to_binary(&get_amount(deps)?),
    }
}

// Declare message header in msg.rs
use ...

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum QueryMsg {
    // GetCount returns the current count as a json-encoded number
    GetAmount {},
}

Rust Code (cosmwasm)

As you can see, in cosmwasm, there is a lot of organization and declaration that the developer needs to handle. In Solidity, this is all done automatically for the developer when they compile their code. However, it is worth noting that Solidity is specially designed and developed over a long time, allowing developers to enjoy the ease of coding today. Builders will continue building for cosmwasm and it is possible that these complexities can be abstracted one day.

Details

Terra's smart contract is split into several portions usually:

  • contract.rs Entry point of the code

    • execute.rs Contract interaction that modifies the state (Cost gas fees)

    • query.rs Contract interaction that reads the state (Does not cost gas fees)

  • error.rs Define your error codes

  • lib.rs Linking your rust files

  • test.rs QA's job

  • state.rs Define your state, data you want to store in smart contracts

  • msg.rs Define your method signature for contract interactions (execute, query, instantiate)

Top level

use ...

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
    deps: DepsMut,
    _env: Env,
    info: MessageInfo,
    msg: InstantiateMsg,
) -> Result<Response, ContractError> {
    let state = State {
        count: msg.count,
        owner: info.sender.clone(),
    };
    set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
    STATE.save(deps.storage, &state)?;

    Ok(Response::new()
        .add_attribute("method", "instantiate")
        .add_attribute("owner", info.sender)
        .add_attribute("count", msg.count.to_string()))
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
    deps: DepsMut,
    _env: Env,
    info: MessageInfo,
    msg: ExecuteMsg,
) -> Result<Response, ContractError> {
    match msg {
        ExecuteMsg::Increment {} => try_increment(deps),
        ExecuteMsg::Reset { count } => try_reset(deps, info, count),
    }
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
    match msg {
        QueryMsg::GetCount {} => to_binary(&query_count(deps)?),
    }
}

contract.rs

In contract.rs we can see what functions we can call on the smart contract. In this example, we can perform get_count, increment, reset on the smart contract. And of course instantiate the smart contract.

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

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct InstantiateMsg {
    pub count: i32,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ExecuteMsg {
    Increment {},
    Reset { count: i32 },
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum QueryMsg {
    // GetCount returns the current count as a json-encoded number
    GetCount {},
}

// We define a custom struct for each query response
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct CountResponse {
    pub count: i32,
}

msg.rs

In msg.rs we declare the method signature of the smart contracts, which we will also be using to generate the schema to interact with the smart contract. So, for example, we want to call the reset function in the smart contract, we do something like the following when broadcasting the txn:

{
    "execute_msg": {
        "reset": {
            "count": 0
        }
    }
}

But in reality, when dealing with sdk or executing the txn from browser, we do not need to specify whether a transaction belongs to instantiation, execution or query as there are specific APIs in place for all different scenarios.

Execute on Terra Station
Execute on Terra Station
Instantiate on Terra Station
Instantiate on Terra Station

Core

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

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

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct State {
    pub count: i32,
    pub owner: Addr,
}

pub const STATE: Item<State> = Item::new("state");

state.rs

This is where you declare all the variables you want to have in the smart contracts. It will be used to store information that you want to persist in smart contracts. In the above example, we want to keep track of a variable called State with members count and owner which is of integer and address type respectively.

pub fn try_increment(deps: DepsMut) -> Result<Response, ContractError> {
    STATE.update(deps.storage, |mut state| -> Result<_, ContractError> {
        state.count += 1;
        Ok(state)
    })?;

    Ok(Response::new().add_attribute("method", "try_increment"))
}

pub fn try_reset(deps: DepsMut, info: MessageInfo, count: i32) -> Result<Response, ContractError> {
    STATE.update(deps.storage, |mut state| -> Result<_, ContractError> {
        if info.sender != state.owner {
            return Err(ContractError::Unauthorized {});
        }
        state.count = count;
        Ok(state)
    })?;
    Ok(Response::new().add_attribute("method", "reset"))
}

execute.rs

In execute.rs is where we finally get into coding the logic out! And this is just your typical rust coding with the exception of retrieving and persisting the state through using the deps variable.

fn query_count(deps: Deps) -> StdResult<CountResponse> {
    let state = STATE.load(deps.storage)?;
    Ok(CountResponse { count: state.count })
}

query.rs

query.rs is where we perform queries on various states about the smart contract. This is usually just getter functions unless we want something more complicated.

Fluff

use cosmwasm_std::StdError;
use thiserror::Error;

#[derive(Error, Debug)]
pub enum ContractError {
    #[error("{0}")]
    Std(#[from] StdError),

    #[error("Unauthorized")]
    Unauthorized {},
    // Add any other custom errors you like here.
    // Look at https://docs.rs/thiserror/1.0.21/thiserror/ for details.
}

error.rs

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

pub use crate::error::ContractError;

lib.rs

These are just fluff stuff we need to put. I don't think we need to talk much about this.