Estimated time: 5 minutes read
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
A complete example of how to program unit tests in Hardhat, based on the details provided, would be the OpenZeppelin ERC721 token repository.
An interesting feature of these test suites is the use of the hardhat-exposed package, which allows direct instantiation of abstract contracts and even access to internal functions in tests using the “$” prefix.
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);
}
- Use the hardhat-tracer tool to add debugging information about call reads and transactions in unit tests. It’s useful for identifying which call may have caused a particular test to fail. You need to install the package, add it to the “hardhat.config.js” file, and invoke the tool during tests:
yarn add --dev hardhat-tracer => add package to the project
require("hardhat-tracer"); => import package to "hardhat.config.js"
yarn hardhat test --trace => run tests invoking hardhat-tracer
How to ensure that all the code has been tested?
Ideally, the scope of the unit tests developed should cover the entirety of the programmed Smart Contract code.
While there are procedures and techniques to structure the code and its corresponding tests optimally to achieve maximum test coverage, having a tool to support this task can be very helpful.
In Hardhat, the solidity-coverage plugin can be used. Its installation and usage follow the standard guidelines:
yarn add --dev solidity-coverage => add package to the project
require("solidity-coverage"); => import package to "hardhat.config.js"
yarn hardhat coverage => run coverage tests
The plugin execution runs all the tests defined in the project and analyzes which parts of the code have not been covered.
As a result, a summary report is displayed in the console, and a detailed HTML report is generated in the coverage directory. You can navigate through the test file tree to see in detail which parts of the code still need to be tested to achieve 100% coverage in all categories.
Using the “.solcover.js” configuration file, it is possible to modify some functionality parameters of the tool, such as excluding specific Solidity source files from the coverage analysis.
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!