Victor Jonah I am a Software Developer with over three years of experience working with JavaScript and its frameworks. I currently work as a remote software developer for a tech agency.

Developing Terra smart contracts

8 min read 2386

Developing Terra Smart Contracts

Building your own smart contracts has been around since the inception of Web 3, allowing people to build programs to deploy to a blockchain.

The blockchains that deploy smart contracts range from Ethereum to Bitcoin, and beyond. Furthermore, smart contracts work together to make up decentralized applications, which can be developed on frameworks like Truffle, Hardhat, and Embark.

In this article, we will look at how we can develop smart contracts and deploy them to the Terra blockchain network, which is similar to Ethereum.

A basic overview of Terra

A few things to note about Terra before reading on:

  • Smart contracts deployed to the Terra blockchain are written in Rust
  • our local testnet is LocalTerra
  • Terra.js and Terra SDK are the two available libraries used to interact with the Terra blockchain
  • Terra was created by Terraform Labs

Also, note that Terra’s consensus is the proof-of-stake algorithm using the Tendermint BFT. This allows holders to stake their tokens as collateral to validate transactions. Rewards are given afterward in accordance with the amount of tokens staked.

It’s also important to know that LUNA is the cryptocurrency of Terra and is used to power the proof-of-stake blockchain. This blockchain offers price stability by expanding algorithmically and reducing supply.

To understand more, I suggest you read the documentation here. Our main focus for this article is how we can deploy our smart contract to this blockchain protocol.

And finally, the Terra blockchain boasts of a strong and vigorous consensus mechanism that finalizes batches of transactions in seconds—faster than Bitcoin and Ethereum.

Requirements and development basics

Let us look at the requirements needed to build and deploy smart contracts using the Terra protocol:

  • Rust programming language knowledge
  • Familiarity with the Terra ecosystem
  • Docker installed on your computer
  • Terra core
  • LocalTerra

However, don’t worry about the above requirements if you do not have them, we will cover them all. One last requirement, however, needed according to the Terra team, is to have the desire to disrupt/disturb traditional finance.

We made a custom demo for .
No really. Click here to check it out.

Environment setup

There are a few things we need to install before we begin. This installation will help us connect to Terra’s local testnet when writing contracts and provide the latest version of terrad. terrad works with the Terra core.

You will also need to install Rust if you don’t have it already.

To begin, let’s install Terra Core, which requires us to install Go first. To do this, download Go using this link and verify:

➜  ~ go version
go version go1.17 darwin/amd64

Go of version v1.17+, which is required to use Terra Core.

Installing Terra Core

Install Terra Core by cloning the repository from GitHub. Then, check out to the main branch which has the latest release:

$ git clone terra-core
$ cd terra-core
$ git checkout main

Next, install Terra Core to get terrad, which will serve as the executable to interact with the Terra node:

$ make install

Then, verify that you have installed it successfully:

$ terrad version --long

Your output should look similar to the following:

name: terra
server_name: terrad
version: 0.5.12-1-gd411ae7
commit: d411ae7a276e7eaada72973a640dcab69825163f
build_tags: netgo,ledger
go: go version go1.17 darwin/amd64

Installing LocalTerra

LocalTerra will be our testnet to test our smart contracts while developing. Our local testnet consists of the WebAssembly integration. To spin up LocalTerra, you need to have Docker and docker-compose set up because LocalTerra is containerized:

$ git clone --depth 1
$ cd LocalTerra

With Docker running in the background, run the following command:

$ docker-compose up

You should get the response below, which are logs:

 11:25PM INF Timed out dur=4955.7669 height=5 module=consensus round=0 step=1
terrad_1         | 11:25PM INF received proposal module=consensus proposal={"Type":32,"block_id":{"hash":"54D9C757E9AA84E0F5AAA736E6EED3D83F364A3A62FDC625970539CA81DFA86E","parts":{"hash":"2517579A126AC2BF6EB9EB6274FAE6748D14115C91FC59FE3A2AF5F061A12740","total":1}},"height":5,"pol_round":-1,"round":0,"signature":"AMxXngubsUHyterTZuZsiLgY0olPDpdpgjMIRZ9L59UR9+JngC93xO63yTxwE0kQLp2HdZ99G8M4ATchS7d1CA==","timestamp":"2021-12-16T23:25:00.8000592Z"}
terrad_1         | 11:25PM INF received complete proposal block hash=54D9C757E9AA84E0F5AAA736E6EED3D83F364A3A62FDC625970539CA81DFA86E height=5 module=consensus
terrad_1         | 11:25PM INF finalizing commit of block hash=54D9C757E9AA84E0F5AAA736E6EED3D83F364A3A62FDC625970539CA81DFA86E height=5 module=consensus num_txs=0 root=84C2F2EF6B7FC8B3ACED8B2B0D2921D649F13CE54C5AB5B032DE988D1392E0FD
terrad_1         | 11:25PM INF minted coins from module account amount=226569846uluna from=mint module=x/bank
terrad_1         | 11:25PM INF executed block height=5 module=state num_invalid_txs=0 num_valid_txs=0
terrad_1         | 11:25PM INF commit synced commit=436F6D6D697449447B5B32382031303020373220323137203234312038352031363320313520313530203137382031353820323235203133312032343620313538203235322031333420313238203134392031383220323033203131372039382031333420312035382032333720323120333620313534203136203134335D3A357D
terrad_1         | 11:25PM INF committed state app_hash=1C6448D9F155A30F96B29EE183F69EFC868095B6CB756286013AED15249A108F height=5 module=state num_txs=0
terrad_1         | 11:25PM INF indexed block height=5 module=txindex
terrad_1         | 11:25PM INF Ensure peers module=pex numDialing=0 numInPeers=0 numOutPeers=0 numToDial=10
terrad_1         | 11:25PM INF No addresses to dial. Falling back to seeds module=pex
terrad_1         | 11:25PM INF Timed out dur=4975.4085 height=6 module=consensus round=0 step=1
terrad_1         | 11:25PM INF received proposal module=consensus proposal={"Type":32,"block_id":{"hash":"5FE8526C43C0B32BEF011299D67FDA44DBD625E0B69836D175C25C1F914DD06E","parts":{"hash":"BE583EC25B30F52E652FA28DEAB869D98602B3FB82CD0D9C80ADF96A210CC8D4","total":1}},"height":6,"pol_round":-1,"round":0,"signature":"Bx3WaDl3hhR9IkDjXRa+dXkSIK0Tezl07gZhDm4RXyJyHq0oriAkQD23Q9+ly1+cFhGIdKF3hyvH3GcjCNLvAQ==","timestamp":"2021-12-16T23:25:05.823444Z"}

Now, we are connected to the LocalTerra network. The following are the port numbers to connect with:

Installing Rust

Rust is what Terra chose to use and write smart contracts with because Rust can compile to WebAssembly and the WebAssembly tooling is well matured and built for Terra.

To install Rust on MacOS or any Linux-like OS, run the following command:

$ curl --proto '=https' --tlsv1.2 -sSf | sh

If you are using Windows, use this link.

Once successfully installed, we must add the wasm32-unknown-unknown target for compilation:

$ rustup target add wasm32-unknown-unknown

Lastly, let’s install cargo-generate to scaffold a CosmWasm smart contract template and cargo-run-script to optimize our smart contracts:

$ cargo install cargo-generate --features vendored-openssl
$ cargo install cargo-run-script

We’re finally done with the installations 🙂!

Writing and exploring a smart contract in Terra

With our LocalTerra network running and waiting for us, we are ready to write a little smart contract, deploy them, and we are done for the day!

Since we installed cargo-generate, we can quickly scaffold a working project. This will help us with the folder structure to write our contracts. To do this, use the following command:

$ cargo generate --git --name PROJECT_NAME

For PROJECT_NAME, you should name it the name of your project. Below is what you should get after running the previous command:

Vectormikes-MacBook-Pro:Projects macbookpro$ cargo generate --git --name terra-demo
🔧   Generating template ...
[ 1/34]   Done: .cargo/config
[ 2/34]   Done: .cargo
[ 3/34]   Skipped: .circleci/config.yml
[ 4/34]   Done: .circleci
[ 1/34]   Done: .cargo/config
[ 2/34]   Done: .cargo
[ 3/34]   Skipped: .circleci/config.yml
[ 4/34]   Done: .circleci
[ 5/34]   Done: .editorconfig
[ 6/34]   Done: .github/workflows/Basic.yml
[ 7/34]   Done: .github/workflows
[ 8/34]   Done: .github
[ 9/34]   Done: .gitignore
[10/34]   Done: .gitpod.Dockerfile
[11/34]   Done: .gitpod.yml
[ 1/34]   Done: .cargo/config
[ 2/34]   Done: .cargo
[ 3/34]   Skipped: .circleci/config.yml
[ 4/34]   Done: .circleci
[ 5/34]   Done: .editorconfig
[ 6/34]   Done: .github/workflows/Basic.yml
[ 7/34]   Done: .github/workflows
[ 8/34]   Done: .github
[ 9/34]   Done: .gitignore
[10/34]   Done: .gitpod.Dockerfile
[11/34]   Done: .gitpod.yml
[12/34]   Done: Cargo.lock
[13/34]   Done: Cargo.toml
[14/34]   Done:
[15/34]   Done:
[16/34]   Done: LICENSE
[17/34]   Done: NOTICE
[18/34]   Done:
[19/34]   Done:
[20/34]   Done: examples/
[21/34]   Done: examples
[22/34]   Done: rustfmt.toml
[23/34]   Done: schema/count_response.json
[24/34]   Done: schema/execute_msg.json
[25/34]   Done: schema/instantiate_msg.json
[26/34]   Done: schema/query_msg.json
[27/34]   Done: schema/state.json
[28/34]   Done: schema
[29/34]   Done: src/
[30/34]   Done: src/
[31/34]   Done: src/
[32/34]   Done: src/
[33/34]   Done: src/
[34/34]   Done: src
🔧   Moving generated files into: `/Users/macbookpro/Desktop/Projects/terra-demo`...
✨   Done! New project created /Users/macbookpro/Desktop/Projects/terra-demo

Looking at the src/ file, we can see three kinds of messages we can send to our smart contract. First, this includes InstantiateMsg, which sets the state in the smart contract, meaning an initial state must be given to the smart contract when it is spun up.

Second, ExecuteMsg is a message that executes an action to the change of state, such as posting a message to the blockchain. And finally, QueryMsg is just as it sounds: it works as a query to the chain, getting data from it.

Let’s see how to use these are used within the code:

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,

Before we move to the contract, let’s look at what interface we have in our State within the src/ file:

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");

Our struct, State, should contain a count of i32 and the owner of Addr. According to Terra, our state is persistent because of Terra’s active LevelDB, which is a key-value storage.

With that in mind, our contract sits in the src/ file:

#[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(, CONTRACT_NAME, CONTRACT_VERSION)?;, &state)?;
        .add_attribute("method", "instantiate")
        .add_attribute("owner", info.sender)
        .add_attribute("count", msg.count.to_string()))

The instantiate method expects four arguments, deps, _env, info, and msg, with their supporting interfaces. We then expect a result which will either be our expected Response or ContractError.

Here, we defined our ContractError in the src/ file:

use cosmwasm_std::StdError;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ContractError {
    Std(#[from] StdError),
    Unauthorized {},
    // Add any other custom errors you like here.
    // Look at for details.

A few other interfaces like Response were also imported from cosmwasm_std:

#[cfg(not(feature = "library"))]
use cosmwasm_std::entry_point;
use cosmwasm_std::{to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult};
use cw2::set_contract_version;

Next, for methods, we also have execute and query in our contract:

#[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),

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

Here, try_increment, which increases the count state by 1, and try_reset, which resets the count state, are functions used in the execute function.

Lastly, the query count, which gets the state or information from the storage for us, can be seen executed below:

#[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)?),
fn query_count(deps: Deps) -> StdResult<CountResponse> {
    let state = STATE.load(;
    Ok(CountResponse { count: state.count })

Uploading the smart contract to LocalTerra

We can now build our smart contract to check for errors during compilation so we can fix it. To do that, we run the following:

$ cargo wasm

And just as we installed cargo-run-script to help optimize our builds, we must now use it:

$ cargo run-script optimize

In the project directory, we should see the code in artifacts/terra_demo.wasm, which we will upload to LocalTerra shortly. We can now create a local testnet name and node moniker:

$ terrad init --chain-id=<testnet_name> <node_moniker>

This will then prompt you to enter your mnemonic:

 $ terrad keys add <account_name>

According to Terra, the account with the below mnemonic is the only validator on the network:

satisfy adjust timber high purchase tuition stool faith fine install that you unaware feed domain license impose boss human eager hat rent enjoy dawn

Next, we can upload the code to the Terra network:

terrad tx wasm store artifacts/terra_demo.wasm --from demo --chain-id=localterra --gas=auto --fees=100000uluna --broadcast-mode=block

This will ask for permission in which you should type “yes” to allow it to push to LocalTerra. If successful, your contract is now broadcast to the LocalTerra network.


We have just begun to scratch the surface of smart contracts in Terra, and this should only give an overview of how to build a dApp on the Terra protocol.

Building on this protocol is highly recommended because of its active growing user base and Terra stablecoins, including LUNA, are being added into payment solutions which is good news.

It also has fast and efficient consensus with more efficiency coming in future releases.

WazirX, Bitso, and Coinsquare use LogRocket to proactively monitor their Web3 apps

Client-side issues that impact users’ ability to activate and transact in your apps can drastically affect your bottom line. If you’re interested in monitoring UX issues, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web and mobile apps, recording everything that happens in your web app or site. Instead of guessing why problems happen, you can aggregate and report on key frontend performance metrics, replay user sessions along with application state, log network requests, and automatically surface all errors.

Modernize how you debug web and mobile apps — .

Victor Jonah I am a Software Developer with over three years of experience working with JavaScript and its frameworks. I currently work as a remote software developer for a tech agency.

Leave a Reply