How to Debug Smart Contracts with Hardhat?

There are multiple tools for debugging smart contracts with Hardhat, both systematically and more casually.

Systematic testing can be accomplished by coding use cases as unit tests, similar to classic programming environments. This way, you can design test suites that validate the various functions developed in the smart contracts, achieving the degree of coverage you deem appropriate.

These tests are very useful for correcting programming errors, ensuring the main use cases of the developed functionalities, and testing interaction sequences with the contracts.

More casual debugging can be done by interacting with the contracts manually through the Hardhat Console or by programming scripts to run repeatedly over time. This type of testing will be analyzed in another subsequent article.

It is recommended to review the article on developing smart contracts with Hardhat to refresh the general details about the installation and configuration of Hardhat.

How to Write Unit Tests with Hardhat?

The general dynamics of unit tests with Hardhat are governed by the principles of the Mocha testing framework.

In general, each group of tests is enclosed within a “describe” block. Each test within that block is defined within an “it” block.

To execute preconditions and postconditions in a controlled manner at the beginning and end of each test, the “before/beforeEach” hooks (before the first test of the block/before each test of the block) and “after/afterEach” hooks (after the last test of the block/after each test of the block) are used.

Preconditions can be useful for initializing the network node in a known state where the tests are executed or to establish a known initial state at the beginning of each test. Although this has been a common practice since the days of Truffle, the Hardhat environment presents a more optimal and efficient possibility, known as Hardhat fixtures.

Postconditions are typically used to invoke cleanup functions at the end of the test execution.

Test result checks generally use assertions and functions from the Chai library. However, it is advisable to install and define the “hardhat-chai-matchers” plugin as a dependency in the “hardhat.config.js” file to add Ethereum-specific capabilities to the Chai library. For this library’s functions to work correctly, it is necessary to configure Hardhat in auto-mining mode (default mode), meaning each transaction in a new block.

All these concepts can be expanded upon on the official Hardhat page for debugging contracts.

Tests are grouped into suites in separate files in the directory defined for tests in the “hardhat.config.js” file (default directory is “test”).

They are executed as follows:

yarn hardhat test .\test\test_suite.js => executes just the test suite on "test_suite.js" file
yarn hardat test => executes every test file

What Configuration Options Can Be Applied?

The most interesting configuration options for the “hardhat.config.js” file from a testing perspective are as follows:

  • loggingEnabled (true/false): Controls whether the Hardhat Network component will log debugging information for each JSON-RPC request to the shell console. By default, it is disabled when launching the Hardhat Network as an ephemeral node and enabled for long-running processes.
  • throwOnTransactionFailures (true/false): Controls whether the Hardhat Network component will throw an exception in case of an error in a write transaction on the blockchain (default mode) to facilitate test verification. This way, you can use the “to.be.reverted” functions from the “hardhat-chai-matchers” library to ensure that appropriate errors were thrown during transaction execution.
  • throwOnCallFailures (true/false): Controls whether the Hardhat Network component will throw an exception in case of an error in a read (call) on the blockchain (default mode) to facilitate test verification. It shares utility with the previous case.

Although the default values for the presented options are generally the most suitable, an example “hardhat.config.js” file is attached, illustrating the explicit definition. It can be seen that the options apply to the network identified as “hardhat” in the “networks” section:

require("@nomicfoundation/hardhat-toolbox");
require("@nomicfoundation/hardhat-chai-matchers");

module.exports = { 
	networks: {
      hardhat: {
        loggingEnabled: false,
        throwOnTransactionFailures: true,
        throwOnCallFailures: true
      },
	},	  
    solidity: {
      version: "0.8.23"
    },       
    paths: {
      sources: "./contracts",
      tests: "./test",
      cache: "./cache",
      artifacts: "./artifacts"
    }
}

What other logging options are available for debugging?

In addition to the mentioned configuration option “loggingEnabled” in the “hardhat.config.js” file, there are other possibilities:

  • Run the tests with the “–verbose” modifier. Much more debugging information will be displayed in the shell console.
yarn hardat test --verbose
  • Add calls to “console.log” from the Solidity smart contract code. It acts like the homonymous function in Node.js. The output will appear mixed with that of the test execution. It is necessary to import the contract “hardhat/console.sol”. Full details can be found in the Hardhat reference documentation. An example directly taken from the official Hardhat page would be the following:
import "hardhat/console.sol";

function transfer(address to, uint256 amount) external {
    require(balances[msg.sender] >= amount, "Not enough tokens");

    console.log(
        "Transferring from %s to %s %s tokens",
        msg.sender,
        to,
        amount
    );

    balances[msg.sender] -= amount;
    balances[to] += amount;

    emit Transfer(msg.sender, to, amount);
}

Next steps: script execution

Hardhat provides multiple resources for debugging smart contracts both systematically and manually.

If you choose to develop test suites using unit tests, the programming approach is very similar to other classic development environments, including the now obsolete tool Truffle, which has been used since the early days of smart contract programming.

This article has covered the fundamentals of unit test programming with Hardhat as well as some useful configuration options.

In future articles, other phases of the testing process will be addressed, such as script execution and using the console to debug transactions.


Do you usually write systematic tests for your smart contracts? Do you use any other tools? Share your experiences by leaving a comment.

If you need a smart contract developer, contact me: my working method always includes ensuring the programmed functionality with automated tests. Thank you!