Smart Contracts for Cosmos Blockchain

Smart contracts, web3, decentralized finance and NFTs have gained popularity once again in the past few years. For a consulting contract, I worked on the implementation of a smart contract for a web3 game. The smart contract functioned on a Cosmos-based blockchain.

What is a Smart Contract? What can they be used for?

A smart contract is a program that runs on a blockchain. Smart contracts can have custom logic and their own method of keeping track of who owns what.

They are used for NFTs and decentralized financial protocols. All collectible NFTs on Ethereum and on various Cosmos-based blockchains are kept track of within a smart contract.

DeFi protocols use smart contracts for keeping track of loan collateral, wrapped tokens, swaps, trades, decentralized exchanges and more. Smart contracts are executed on nodes within the blockchain and can be queried for data and information.

What is Cosmos?

Cosmos is a blockchain that makes building new chains easier than before, also known as an internet of blockchains. Cosmos functions as a foundation that can be used to build application-specific blockchains that are interoperable.

The primary token for each chain is used to pay gas fees (gas keeps track of the amount of resources used during execution of a transaction). The cryptocurrency token for Cosmos is ATOM, and each application-specific chain has their own tokens such as STARS for Stargaze, JUNO for Juno, OSMO for Osmosis, and SCRT for Secret Network.

Cosmos supports smart contracts through CosmWasm. You can write smart contracts using the Rust programming language for Cosmos-based blockchains that support CosmWasm.

Which Cosmos-based blockchains support CosmWasm?

The following Cosmos-based blockchains support running CosmWasm smart contracts:

This list is based on the documentation for the chains and based on the Cosmos chain registry repo on GitHub (where the schema will contain cosmwasm_enabled).

How do you write smart contracts on Cosmos? How do you write an NFT or tokens contract?

You can write smart contracts for Cosmos-based blockchains using the Rust programming language and the CosmWasm SDK and libraries. Rust is a typesafe and performant language that can be used to write any sort of software from web servers to game engines to smart contracts. It improves the reliability of programs and can be used in resource-constrained environments where efficiency is a priority.

Learn more about Rust

Here are the base libraries you will need to get started with writing CosmWasm smart contracts:

  • cosmwasm-std contains standard utility functions for working with wallet addresses and coins
  • cosmwasm-storage contains code for storing data as maps/dictionaries/hashes and as individual values
  • cosmwasm-schema helps generate the JSON schema for interacting with the smart contract

If you want to write an NFT smart contract, your contract’s interface will need to adhere to the CW721 interface. You can use the cw721-base code to build your own NFTs smart contract.

NFTs are non-fungible tokens, each NFT has a unique identifier and belongs to one owner. Some popular examples of NFTs are Cryptokitties, Doodles, CryptoPunks, BAYC (Bored Ape Yacht Club), and Loot. Your NFT smart contract can include code that calculates royalties whenever an NFT is bought or sold, or it can include additional details about the NFT such as the traits or image data to use. A really creative developer can write Rust code that evolves the NFT over time or based on certain interactions. For instance, every time the NFT is traded, one of the traits could have a new value or the image data could be modified.

If you would like to create fungible tokens, and issue your own cryptocurrency, you can do that by writing a smart contract that adheres to the CW20 specification.

When you write your smart contract, you can re-use code from other smart contracts by adding their crates to your Rust project. As mentioned above, there are standard libraries that you can use for CosmWasm, and there are additional packages that can be used as a base for CW20 fungible tokens and CW721 Non-Fungible Tokens.

During a smart contract consulting project, we implemented a way for NFTs of one type to be exchanged for another type of NFT that was randomly chosen from the set of remaining unowned NFTs. We had to adhere to the CW721 NFT contract interface to make it work.

How does a Cosmos smart contract interact with another smart contract?

Interactions between smart contracts allow composability of contracts, where you can build on top of NFT collections and DeFi protocols.

As part of an NFT consultation and implementation project, we had to interact with other smart contracts. One of the interactions was with a CW721 NFT smart contract in order to verify ownership of an NFT that would be exchanged. Another interaction was with a randomness oracle smart contract, LoTerra, which would return a random number. As part of the smart contract execution, the query messages would be sent out to the other smart contracts, return with responses and continue on the happy path to update the ownership of the NFTs.

To interact with another contract, you can execute a message, which can change state and update the storage in the other contract, or you can query that contract for information. In the Rust smart contract code, this is implemented by creating new message structs that are converted and passed along to the CosmWasm querier.

This code shows how to build a query message that queries another smart contract:

fn is_airdrop_eligible(deps: Deps, owner: String) -> bool {
    let msg = hackathon_nfts::msg::QueryMsg::Balance { owner, token_id: "example".to_string() };
    let wasm = WasmQuery::Smart {
        contract_addr: HACKATHON_NFTS_CONTRACT_ADDR.to_string(),
        msg: to_binary(&msg).unwrap(),
    };
    let res: StdResult<hackathon_nfts::msg::BalanceResponse> = deps.querier.query(&wasm.into());
    match res {
        Ok(res) => res.balance.u128() == 1u128,
        _ => false,
    }
}

Unit Testing and Mocks and Test Data for CosmWasm smart contracts

What’s great about Rust is that you can add unit tests with mocks to thoroughly test various query and execute responses from another smart contract.

Here is an example of a mock querier used in a unit test where the response is mocked with fake data for testing:

#[derive(Clone, Default, Serialize)]
pub struct HackathonNftsBalanceResponse {
    pub balance: Uint128,
}

impl HackathonNftsBalanceResponse {
    pub fn new(balance: Uint128) -> Self {
        HackathonNftsBalanceResponse { balance }
    }
}

// ...

impl WasmMockQuerier {
    pub fn handle_query(&self, request: &QueryRequest<TerraQueryWrapper>) -> QuerierResult {
        match &request {
            QueryRequest::Wasm(WasmQuery::Smart { contract_addr, msg }) => {
                if contract_addr == &"terra1utd7hcq00p0grm08uyg23mdm782726y3euk8pq".to_string() {
                    let msg_hackathon_nfts_balance = HackathonNftsBalanceResponse {
                        balance: Uint128::from(1u128)
                    };
                    return SystemResult::Ok(ContractResult::from(to_binary(&msg_hackathon_nfts_balance)));
                }
                panic!("DO NOT ENTER HERE");
            },
            _ => self.base.handle_query(request),
        }
    }

    pub fn new(base: MockQuerier<TerraQueryWrapper>) -> Self {
        WasmMockQuerier { base }
    }
}

Through unit testing, it’s possible to test a large variety of different parameters and thanks to Rust’s speed and performance, you can write more tests and create more test data for more thorough test scenarios.

Here is an example of a unit test for a Cosmos smart contract:

#[cfg(test)]
mod tests {
    use super::*;
    use crate::mock_querier::mock_dependencies_custom;
    use cosmwasm_std::testing::{mock_dependencies, mock_info, MOCK_CONTRACT_ADDR};
    use cosmwasm_std::{
        coins, from_binary, Addr, BlockInfo, ContractInfo,
    };

    // ...

    #[test]
    fn mint_pizza_and_pie() {
        let mut deps = mock_dependencies_custom(&[]);
        let res = instantiate(
            deps.as_mut(),
            mock_env(0),
            mock_info("creator_address", &coins(1000, "earth")),
            InstantiateMsg {},
        ).unwrap();
        assert_eq!(0, res.messages.len());

        // regular mint before aidrop is claim gives two pizzas
        let res = execute(
            deps.as_mut(),
            mock_env(1),
            mock_info(
                "player_address",
                &[Coin {
                    amount: Uint128::from(1_000_000u128),
                    denom: "uusd".to_string(),
                }],
            ),
            ExecuteMsg::MintPizza {},
        ).unwrap();
        assert_eq!(0, res.messages.len());

        let res = query(
            deps.as_ref(),
            mock_env(2),
            QueryMsg::Inventory {
                address: "player_address".to_string(),
            },
        ).unwrap();
        let inventory: InventoryResponse = from_binary(&res).unwrap();
        println!("{:?}", inventory);
        assert_eq!(2, inventory.pizzas.len(), "there should be two pizzas from mint + airdrop");
        assert_eq!(0, inventory.pies.len(), "there should be no pies, no pizzas have been combined yet");

        // ...
    }
}

One great benefit of CosmWasm and Cosmos-based blockchains is that you can run an array of tests within a local development testnet to ensure that interactions with your smart contract are working correctly in terms of transactions, wallet balances and storage updates. Local development testnets provide a way to test for various scenarios. It’s recommended to test thoroughly on a testnet and, especially for DeFi protocols, to hire an auditing firm to audit the smart contract code.

Here is an example of how to run JavaScript code that executes transactions on a local testnet:

const coins = { uluna: 1_000_000, uusd: 1_000_000 };
console.log('sending coins from multiple senders to one receiving wallet');
for (let wallet of wallets) {
    if (wallet.address === wallets[0].address) {
        continue;
    }

    const send = new MsgSend(
        wallet.address,
        wallets[0].address,
        coins
    );
    try {
        const tx = await terraWalletFromMnemonic(wallet.mnemonic).createAndSignTx({
            msgs: [send],
            memo: 'terrajs-test-harness',
            fee: new Fee(2_000_000, { uusd: 10_000_000 })
        });
        const result = await terra.tx.broadcast(tx);
        await displayTxInfo(result.txhash);
    } catch (e) {
        console.log(e);
    }
}