Testing & Debugging Smart Contracts (InterWasm DEV #1)

7 Techniques for CosmWasm {ATOM, JUNO, SCRT, OSMO, LUNA & more}

Peter Keay
Obi.Money
9 min readFeb 11, 2022

--

Smart contracts are, at their core, simple pieces of code.

No frills, no gimmicks, no UI glitz or graphic assets, no struggling with variants of -webkit- and adapting your app to work on all possible sizes of iOS and every version of Internet Explorer.

The usual challenge of smart contract writing is not complexity. A typical smart contract only:

  • initializes with some defaults (InstantiateMsg)
  • accepts and processes input from admins and/or users (ExecuteMsg)
  • handles funds or NFTs received (Bank, cw20, cw721) and/or saves some resulting data (Storage)
  • returns data when queried (QueryMsg)
  • possibly queries other contracts or sends ExecuteMsgs to other contracts along the way

That last item, multi-contract operations, will be the topic of the next post in this series.

Besides the mindset needed to write decentralized software, the main challenges of smart contract writing are: the precision required, limited tooling & learning materials, and difficult debugging.

These are all areas which are being improved.

Safer templates and helpers & wider knowledge of safe practices will reduce the risk of creating a contract with a money-draining flaw.

Better tools such as terrain, CWScript, and cosmwasm-plus are simplifying work with contracts and easing unit and integration tests. And more complete education is continually being released, at places such as Terra Academy.

Improvements in local test environments and tools such as multi-test will reduce the difficulty of debugging.

But debugging is still a significant challenge.

This article surveys some methods CosmWasm smart contract writers use to test for & track down issues in their contracts.

Note: this is not an article for total newcomers. I assume basic knowledge of Rust (and, for the last item, JavaScript). But even if you’re a beginner, you might want to survey the 7 items below.

1. Local: the Rust Compiler

I’ve built smart contracts on Ethereum, BSC, Tron (I know, I know) with Solidity; IOST with JavaScript; and EOS (I know, I know) with C++.

Last year, I moved entirely over to CosmWasm’s Rust smart contracts.

Why? Because of its widespread usage in CosmosSDK chains, yes — but also for its advantages over other smart contract frameworks.

That’ll be the next post, after the multi-contract one: advantages of CosmWasm.

One of the advantages of CosmWasm’s preferred contract language, Rust, is that the compiler demands precision — and usually provides helpful suggestions when the code has some deficiency.

See that blue text? Much of the time (but not all the time!), that’s what you can do to fix the problem.

In theory, this reduces the number of bugs that show up later, by catching many issues at compile time.

2. Local: Comprehensive Unit Tests

Smart contracts in Rust have test cases built right in. By default, they’re at the bottom of contract.rs, but many writers move them out to their own file.

Writing tests that cover all the success and failure cases of your code may seem tedious, but it’s considered essential by most writers.

When you refactor or add new features, you can be sure that you haven’t broken your contract’s functionality. Tests can also help other people (future maintainers, for example) understand what your contract is doing.

The InterWasm template smart contract includes tests for you to investigate, if you haven’t written tests before.

Unit tests run on your contract every time you compile.

A good double-check is using tarpaulin to see how much of your code is covered by your unit tests.

cargo install cargo-tarpaulin gets you up and running, and then you can (from the project root directory) run cargo tarpaulin -o html to generate some pretty charts.

Open the resulting charts in browser, or from the command line with xdg-open tarpaulin-report.html (Linux) or open tarpaulin-report.html on MacOS. This will show you parts of your code that are not covered by your unit tests.

Remember that you want to test failures.

Some failure cases include:

  • trying to push an ExecuteMsg from an account that shouldn’t be authorized to do so
  • trying to exceed limits, such as maximum counts and expirations
  • trying to input invalid parameters in an ExecuteMsg

On the whole, you can be less comprehensive with QueryMsg tests, since queries cannot change contract state. (Querier “runs provably read-only operations and it is impossible to trigger any reentrancy (recursion) in execute.) Failures to input the correct type in the QueryMsg, for example, will throw an error without risking the contract’s security.

But when it comes to your ExecuteMsg code, you want as many cases covered as possible.

3. Local: Caveman Output with println!

Running cargo test -- --show-output allows you to show the output of any println! statements you make in your contract. The println! macro can conveniently handle many variables with its built-in formatter:

This lets you send any information about the current state of your program to your console. Put any variables in there you would like, and you can potentially figure out bugs if the values that get printed don’t make sense.

Here’s another example. I’m using the ADMIN controller, and I want to get the current ADMIN address and the address that sent the transaction (which is stored as sender in the message info):

This prints out the information I need, as long as I remember to add -- --show-output to my cargo test:

It’s best to use println! right in the code for your test cases, rather than in your main contract code — but sometimes, plunking the caveman code right into the contract is the only way to hunt down a problem.

The println! macro can display more complex data like a struct, too, as long as the struct implements the Debug trait, and as long as you use {:?} in the format string rather than using {}.

Struct definitions in CosmWasm usually include the Debug trait, which allows the following examples.

Result:

{:?} is useful for displaying the type and value of something, too, even if you don’t need to display something like a struct:

Result:

4. Local: Multi-Test

The CosmWasm multi-test package is an essential and helpful piece for developing multi-contract apps, and even though it was mostly omitted from the first version of this article, it’s now my go-to for comprehensive integration tests.

Recent updates have made the old section here irrelevant, so I’m rewriting it as a new piece to be published soon.

5. Testnet: Adding Attributes

Once you have started pushing your contracts to a testnet, debugging becomes more difficult. Without running your own node and jumping other hurdles, you cannot simply println! anymore. But your application may suddenly show errors it has not before — especially if you’re running a multi-contract application.

The first thing to do is add helpful feedback to the attributes of your Response. The default CosmWasm template contract shows you how to do this:

You can even add attributes in a loop — though you’ll have to use a tactic like .clone() in order to avoid moving the Response on the first run through.

Remember that the attributes are also where you can provide useful information to frontend developers. When they send an ExecuteMsg to your contract, they will be able to retrieve these attributes from the response.

Sometimes, you need to show a JSON here — for example, when you’re debugging a message between contracts and want to see the whole message that’s getting dispatched. The serde-json library will fail since it includes floating point operations, which are not supported in CosmWasm contracts, but thankfully, the team has created serde-json-wasm.

Here’s an example of printing out a whole JSON message as an attribute:

This method can provide helpful information about your contract’s operation, assuming the transaction is succeeding. But if your contract is failing and you have no idea where, it’s time for the most tedious method: the caveman fail.

6. Testnet: Caveman Fail

This obviously isn’t ideal, as you’re taking the time to deploy and instantiate your contract on testnet every time you make updates to your code.

But sometimes, it’s hard to figure out where exactly your code is failing.

Someday, better debug information will be available. For now, this is your last resort to narrow down exactly where the bug is occurring: Plant a CavemanError, and if that error throws, you know your code successfully made it at least that far. Rinse and repeat.

7. Testnet: Integration Tests

You can script out integration tests with command line scripts, of course, pushing transactions manually.

But another option is the terrain dev environment, which works on Terra locally, on testnet, and on mainnet. I’m not yet sure how well it works on chains other than Terra. I’ll update here once I’ve given it a try.

Installing terrain

On a machine running npm/nvm, it’s easy to install. (In case you don’t know, don’t type the $ into a command line. It just means “this is a terminal command.”)

Now, you can spin up a new application, including a sample contract, frontend, and deployment scripts, by using:

If you are unable to install an npm package globally, you can do something like this instead:

The terrain development framework allows you to write JavaScript helpers to run everything related to your contract. This can be modest helpers for actions, but it can also ease routine set-up operations:

You could just use let res = await execute_obj.client.execute rather than my custom let res = await transaction_runner, but I like to use a custom transaction executing function.

Since testnets sometimes have errors — in particular the frequent Bombay Testnet sequence mismatch errors right now, due to load balancing — you can use this helper function that puts the transaction together and automatically retry on fail. For example:

You then use your JavaScript helpers for all of your contract functions in order to put together integration tests. Run these manually, or include them in your CI/CD pipeline.

That’s it for now. Which methods do you appreciate the most? Do you have some you use which weren’t covered here?

Peter Keay is Co-Founder at Obi.Money. InterWasm DEV is a series of topical posts on building apps on Cosmos and other blockchains using Rust or Move smart contracts.

--

--

Peter Keay
Obi.Money

Rust/C++/WASM smart contract dev || Creative || Writer