How to Start Developing Smart Contracts for Cosmos
Smart contracts, web3, decentralized finance and NFTs have gained popularity once again in the past few years. For a consulting contract, we worked on the implementation of a smart contract for a web3 game. The smart contract functioned on a Cosmos-based blockchain.
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.
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.
Cosmos supports smart contracts through CosmWasm. You can write smart contracts using the Rust programming language for Cosmos-based blockchains that support CosmWasm. 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.
The following Cosmos-based blockchains support running CosmWasm smart contracts:
- Cudos, a cloud computing blockchain
- Juno, a chain for smart contracts and app development
- Osmosis, a decentralized exchange, recently enabled CosmWasm support
- Secret Network, a privacy-first chain, supports a modified version of CosmWasm
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.
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 = example_nfts::msg::QueryMsg::Balance { owner, token_id: "example".to_string() };
let wasm = WasmQuery::Smart {
contract_addr: EXAMPLE_NFTS_CONTRACT_ADDR.to_string(),
msg: to_binary(&msg).unwrap(),
};
let res: StdResult<example_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.
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_nft() {
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());
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::MintNft {},
).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.nfts.len(), "there should be two nfts from mint + airdrop");
// ...
}
}
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 ExampleNftsBalanceResponse {
pub balance: Uint128,
}
impl ExampleNftsBalanceResponse {
pub fn new(balance: Uint128) -> Self {
ExampleNftsBalanceResponse { balance }
}
}
// ...
impl WasmMockQuerier {
pub fn handle_query(&self, request: &QueryRequest<TerraQueryWrapper>) -> QuerierResult {
match &request {
QueryRequest::Wasm(WasmQuery::Smart { contract_addr, msg }) => {
if contract_addr == &"terra1EXAMPLE".to_string() {
let msg_example_nfts_balance = ExampleNftsBalanceResponse {
balance: Uint128::from(1u128)
};
return SystemResult::Ok(ContractResult::from(to_binary(&msg_example_nfts_balance)));
}
panic!("DO NOT ENTER HERE");
},
_ => self.base.handle_query(request),
}
}
pub fn new(base: MockQuerier<TerraQueryWrapper>) -> Self {
WasmMockQuerier { base }
}
}
Testing in Local Development Testnet
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);
}
}
Article originally published at https://rudolfolah.com/smart-contracts-for-cosmos-blockchain/ by Rudolf Olah