Writing & Deploying CW20 Contract

This guide provides a step-by-step process to write and deploy a CW20 token contract on the MANTRA Chain using Rust and CosmWasm. CW20 contracts are similar to ERC20 contracts on Ethereum and are widely used for token creation and management on Cosmos-based blockchains.

In the previous sections, we have understood the dev environment setup, wallet setup and collect testnet OM tokens for the development purposes. Therefore, ensure your Keplr wallet is set up and contains OM testnet tokens for the deployment process.

Prerequisites

If you encounter any issues with your development environment setup or need to refresh your setup, follow these steps:

  1. Install Rust

First, you need to install Rust, a programming language used for developing smart contracts. You can do this by running the following command in your terminal:

curl --proto '=https' --tlsv1.2 <https://sh.rustup.rs> -sSf | sh

When prompted, choose the first option for standard installation. This will install Rust and its package manager, Cargo.

  1. Set Up Rust and Cargo

After installing Rust, set up Rust to use the stable version and verify the installation:

rustup default stable

cargo version

rustup target add wasm32-unknown-unknown

This ensures that you have the latest stable version of Rust and adds the WebAssembly target required for smart contract development.

  1. Install cargo-generate

cargo-generate is a tool that helps you to generate a new Rust project from a template. Install it using the following command:

cargo install cargo-generate --features vendored-openssl
  1. Install mantrachaind

Download and extract the pre-built mantrachaind binary:

On Linux:

# Download the CLI
curl -LO <https://github.com/MANTRA-Finance/public/raw/main/mantrachain-testnet/mantrachaind-linux-amd64.zip>

# Unzip the CLI
unzip mantrachaind-linux-amd64.zip

On MacOS:

# Download the CLI for Intel chips
curl -LO <https://github.com/MANTRA-Finance/public/raw/main/mantrachain-hongbai/mantrachaind-static-darwin-amd64.tar.gz>

# Download the CLI for Silicon chips (M1, M2...)
curl -LO <https://github.com/MANTRA-Finance/public/raw/main/mantrachain-hongbai/mantrachaind-static-darwin-arm64.tar.gz>

# Extract the CLI
tar -xzvf mantrachaind-static-darwin-*.tar.gz

If you encounter a missing libwasmvm.x86_64.so error, download the library:

sudo wget -P /usr/lib <https://github.com/CosmWasm/wasmvm/releases/download/v2.1.0/libwasmvm.x86_64.so>

For more details on setting up the development environment, check the section Install Prerequisites.


Writing & deployment of CW20 contract

Step 1: Clone the Project

Clone the repository that contains the boilerplate code for the CW20 contract:

git clone <https://github.com/MANTRA-Finance/first_token_cw20contract>

After cloning the repository, open the project in your preferred code editor, such as Visual Studio Code.

Step 2: Update Dependencies

Navigate to the Cargo.toml file in the root directory of your project. This file manages the dependencies for your Rust project. Add the following dependency to include the cw20-base package:

Cargo.toml

[dependencies]
cw20-base = {  version = "2.0.0", features = ["library"] }
cw20 = "2.0.0"
cw-utils = "2.0.0"
cosmwasm-std = "2.1.3"
cosmwasm-storage = "1.1.1"
cw-storage-plus = "2.0.0"
cw2 = "2.0.0"
schemars = "0.8.21"
serde = { version = "1.0.207", default-features = false, features = ["derive"] }
thiserror = { version = "1.0.63" }
cosmwasm-schema = "2.1.3"
prost = "0.13.1"

This ensures that you have the necessary libraries to work with CW20 tokens, providing essential functionalities like token transfer, allowance, and minting.

Now that we have completed the setup , let's modify the boilerplate code and get our token ready .

We will be changing or creating the logic inside the following files :

  • scr/msg.rs

  • src/contract.rs

  • src/lib.rs

Step 3: Modify Contract Files

Update msg.rs

use cosmwasm_schema::cw_serde;

#[cw_serde]
pub enum MigrateMsg {}

Update contract.rs

In the contract.rs file, define the core contract logic. This includes handling instantiation, execution, and query messages:

#[cfg(not(feature = "library"))]
use cosmwasm_std::entry_point;
use cosmwasm_std::{
    to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult,
};
use cw20_base::ContractError;
use cw20_base::enumerable::{query_all_allowances, query_all_accounts};
use cw20_base::msg::{QueryMsg,ExecuteMsg};

use crate::msg::MigrateMsg;
use cw2::set_contract_version;
use cw20_base::allowances::{
    execute_decrease_allowance, execute_increase_allowance, execute_send_from,
    execute_transfer_from, query_allowance, execute_burn_from,
};
use cw20_base::contract::{
    execute_mint, execute_send, execute_transfer, execute_update_marketing,
    execute_upload_logo, query_balance, query_token_info, query_minter, query_download_logo, query_marketing_info, execute_burn,
};

// version info for migration info
const CONTRACT_NAME: &str = "crates.io:cw20-token";
const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(#[cfg(not(feature = "library"))]
use cosmwasm_std::entry_point;
use cosmwasm_std::{to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult};
use cw2::set_contract_version;
use cw20_base::allowances::{
    execute_burn_from, execute_decrease_allowance, execute_increase_allowance, execute_send_from,
    execute_transfer_from, query_allowance,
};
use cw20_base::contract::{
    execute_burn, execute_mint, execute_send, execute_transfer, execute_update_marketing,
    execute_update_minter, execute_upload_logo, query_balance, query_download_logo,
    query_marketing_info, query_minter, query_token_info,
};
use cw20_base::enumerable::{query_all_accounts, query_owner_allowances, query_spender_allowances};
use cw20_base::msg::{ExecuteMsg, QueryMsg};
use cw20_base::ContractError;

use crate::msg::MigrateMsg;

// version info for migration info
const CONTRACT_NAME: &str = "crates.io:cw20-token";
const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    msg: cw20_base::msg::InstantiateMsg,
) -> Result<Response, ContractError> {
    set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
    Ok(cw20_base::contract::instantiate(deps, env, info, msg)?)
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    msg: ExecuteMsg,
) -> Result<Response, ContractError> {
    match msg {
        ExecuteMsg::Transfer { recipient, amount } => {
            execute_transfer(deps, env, info, recipient, amount)
        }
        ExecuteMsg::Burn { amount } => execute_burn(deps, env, info, amount),
        ExecuteMsg::Send {
            contract,
            amount,
            msg,
        } => execute_send(deps, env, info, contract, amount, msg),
        ExecuteMsg::Mint { recipient, amount } => execute_mint(deps, env, info, recipient, amount),
        ExecuteMsg::IncreaseAllowance {
            spender,
            amount,
            expires,
        } => execute_increase_allowance(deps, env, info, spender, amount, expires),
        ExecuteMsg::DecreaseAllowance {
            spender,
            amount,
            expires,
        } => execute_decrease_allowance(deps, env, info, spender, amount, expires),
        ExecuteMsg::TransferFrom {
            owner,
            recipient,
            amount,
        } => execute_transfer_from(deps, env, info, owner, recipient, amount),
        ExecuteMsg::BurnFrom { owner, amount } => execute_burn_from(deps, env, info, owner, amount),
        ExecuteMsg::SendFrom {
            owner,
            contract,
            amount,
            msg,
        } => execute_send_from(deps, env, info, owner, contract, amount, msg),
        ExecuteMsg::UpdateMarketing {
            project,
            description,
            marketing,
        } => execute_update_marketing(deps, env, info, project, description, marketing),
        ExecuteMsg::UploadLogo(logo) => execute_upload_logo(deps, env, info, logo),
        ExecuteMsg::UpdateMinter { new_minter } => {
            execute_update_minter(deps, env, info, new_minter)
        }
    }
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
    match msg {
        QueryMsg::Balance { address } => to_json_binary(&query_balance(deps, address)?),
        QueryMsg::TokenInfo {} => to_json_binary(&query_token_info(deps)?),
        QueryMsg::Minter {} => to_json_binary(&query_minter(deps)?),
        QueryMsg::Allowance { owner, spender } => {
            to_json_binary(&query_allowance(deps, owner, spender)?)
        }
        QueryMsg::AllAllowances {
            owner,
            start_after,
            limit,
        } => to_json_binary(&query_owner_allowances(deps, owner, start_after, limit)?),
        QueryMsg::AllAccounts { start_after, limit } => {
            to_json_binary(&query_all_accounts(deps, start_after, limit)?)
        }
        QueryMsg::AllSpenderAllowances {
            spender,
            start_after,
            limit,
        } => to_json_binary(&query_spender_allowances(
            deps,
            spender,
            start_after,
            limit,
        )?),
        QueryMsg::MarketingInfo {} => to_json_binary(&query_marketing_info(deps)?),
        QueryMsg::DownloadLogo {} => to_json_binary(&query_download_logo(deps)?),
    }
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> StdResult<Response> {
    Ok(Response::default())
}

These functions define the contract's entry points (instantiate, execute, query, migrate) and handle token operations such as transfers, burns, minting, allowances, and querying balances.It uses helper functions from the cw20-base package to handle common CW20 operations.

Error Handling Logic

The error handling on this contract happens at the cw20-base library level, so you don’t need to implement specific errors unless you want to.

Step 4: Build Artifacts and Wasm file

To build the artifact, we need to run the rust-optimizer with docker:

docker run --rm -v "$(pwd)":/code \\
  --mount type=volume,source="$(basename "$(pwd)")_cache",target=/target \\
  --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \\
  cosmwasm/optimizer:0.16.0

Or if you are on MacOS:

docker run --rm -v "$(pwd)":/code \\
  --mount type=volume,source="$(basename "$(pwd)")_cache",target=/target \\
  --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \\
  cosmwasm/optimizer-arm64:0.16.0

This command generates a wasm file first_token_cw20contract.wasm under the artifacts folder.

Step 5: Deploy the contract

There are multiple ways to deploy a contract on chain, but the most common ones are via the mantrachaind cli tools and via scripts (bash, typescript).

  • Using the CLI:

RES=$(mantrachaind tx wasm store artifacts/first_token_cw20contract.wasm --from <wallet> --node <https://rpc.hongbai.mantrachain.io:443> --chain-id mantra-hongbai-1 --gas-prices 0.35uom --gas auto --gas-adjustment 1.4 -y --output json)
TX_HASH=$(echo $RES | jq -r .txhash)
CODE_ID=$(mantrachaind query tx $TX_HASH --node <https://rpc.hongbai.mantrachain.io:443> -o json| jq -r '.logs[0].events[] | select(.type == "store_code") | .attributes[] | select(.key == "code_id") | .value')
echo $CODE_ID

Once we get the code_id after storing the contract on chain, we can create an instance of the contract like this:

MSG='{
  "name": "MANTRACW20",
  "symbol": "MNTRA",
  "decimals": 6,
  "initial_balances": [
    {
      "address": "", // add your address here
      "amount": "10000000"
    }
  ]
}'

mantrachaind tx wasm instantiate <code_id> "$MSG" --from <wallet> --node <https://rpc.hongbai.mantrachain.io:443> --chain-id mantra-hongbai-1 --label "MANTRAcw20" --no-admin --gas-prices 0.35uom --gas auto --gas-adjustment 1.4 -y --output json

This will produce an output like the following:

gas estimate: 217659
{"height":"0","txhash":"BB3A0FEF8370B0A407D8AB93919212A4E39E8BA4D52D84836E2E27D76F707D0A","codespace":"","code":0,"data":"","raw_log":"[]","logs":[],"info":"","gas_wanted":"0","gas_used":"0","tx":null,"timestamp":"","events":[]}

We can get the contract address by exploring the transaction hash using jq:

hamantrachaind q tx BB3A0FEF8370B0A407D8AB93919212A4E39E8BA4D52D84836E2E27D76F707D0A --node <https://rpc.hongbai.mantrachain.io:443> -o json | jq -r '.logs[] | .events[] | select(.type == "instantiate") | .attributes[] | select(.key == "_contract_address") | .value'

Once deployed, you will receive the wallet address and confirmation of contract instantiation. Verify your deployment on the MANTRA Chain Explorer athttp://explorer.hongbai.mantrachain.io.

  • Using typescript:

Create a new folder named “deployer” to manage the deployment scripts:

mkdir deployer

Navigate to the “deployer” folder and initialize it with npm:

cd deployer

npm init -y

npm i

This sets up a new Node.js project in the deployer folder.

Install TypeScript and initialize a TypeScript configuration file:

npm install typescript --save-dev

npx tsc --init

Install the necessary dependencies for the deployment script:

npm install @cosmjs/cosmwasm-stargate

npm install @cosmjs/proto-signing @cosmjs/stargate dotenv fs

Ensure your package.json in the deployer folder includes the following dependencies:

"dependencies": {
    "@cosmjs/cosmwasm-stargate": "^0.32.4",
    "@cosmjs/proto-signing": "^0.32.4",
    "@cosmjs/stargate": "^0.32.4",
    "dotenv": "^16.4.5",
    "fs": "^0.0.1-security"
  }

Now, manually copy the wasm file to the deployer folder, which was created under the artifacts file.

Now create an index.ts file in the deployer folder with the following code for deployment script.

const { DirectSecp256k1HdWallet } = require("@cosmjs/proto-signing");
const {
  assertIsBroadcastTxSuccess,
  SigningCosmWasmClient,
  CosmWasmClient,
} = require("@cosmjs/cosmwasm-stargate");
const { coins, GasPrice } = require("@cosmjs/stargate");
const fs = require("fs");
require("dotenv").config();

const mnemonic = process.env.MNEMONIC;// Replace with your mnemonic
 
const rpcEndpoint = "<https://rpc.hongbai.mantrachain.io>";
const contractWasmPath = "./first_token_cw20contract.wasm"; // Path to your compiled  contract (wasm file)

async function deploy() {
  // Step 1: Set up wallet and client
  const wallet = await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, {
    prefix: "mantra", // Replace with the correct prefix for your chain
  });
  const [account] = await wallet.getAccounts();
  console.log(`Wallet address: ${account.address}`);

  // Step 2: Connect to the blockchain
  const client = await SigningCosmWasmClient.connectWithSigner(
    rpcEndpoint,
    wallet,
    { gasPrice: GasPrice.fromString("0.0025uom") }
  );
  console.log("Connected to blockchain");

  // Step 3: Upload contract
  const wasmCode = fs.readFileSync("./first_token_cw20contract.wasm"); // wasm file
  const uploadReceipt = await client.upload(
    account.address,
    wasmCode,
    "auto",
    "Upload CosmWasm contract"
  );
  const codeId = uploadReceipt.codeId;
  console.log(`Contract uploaded with Code ID: ${codeId}`);

  // Step 4: Instantiate contract
  const initMsg = {
    name: "MANTRAcw20",
    symbol: "MNTRA",
    decimal: 6,
    initial_balances: [
      {
        address: " ", // add your wallet address
        amount: 10000000,
      },
    ],
  }; // Replace with your contract's init message
  const instantiateReceipt = await client.instantiate(
    account.address,
    codeId,
    initMsg,
    "My CW20 contract",
    "auto"
  );
  const contractAddress = instantiateReceipt.contractAddress;
  console.log(`Contract instantiated at reciept: ${instantiateReceipt}`);
  console.log(`Contract instantiated at address: ${contractAddress}`);
}

deploy().catch(console.error);

Remember to add your mnemonic and MANTRA Chain's RPC endpoint “https://rpc.hongbai.mantrachain.io” wherever mentioned. Additionally, include your wallet address in the initMsg structure. Modify contract.rs, index.ts, and msg.rs as per your contract requirements.

Compile TypeScript: Execute the following command to compile index.ts It will automatically create an index.js file upon successful compilation:

tsc index.ts

Deploy Contract: Run the final command to deploy your CW20 contract on the MANTRA Chain. Ensure your wallet contains sufficient OM test tokens:

npx ts-node index.ts

Once deployed, you will receive the wallet address and confirmation of contract instantiation. Verify your deployment on the MANTRA Chain Explorer at http://explorer.hongbai.mantrachain.io.

Last updated