Debugging a sample Hardhat Project
Quick Start
First, make sure you have hardhat installed. If you do not already, you can install it from npm:
npm install --save-dev hardhat
Then initialize a new sample Typescript hardhat project:
npx hardhat
Choose 'Create a Typescript project.'
$ npx hardhat
'888 888 888 888 888
888 888 888 888 888
888 888 888 888 888
8888888888 8888b. 888d888 .d88888 88888b. 8888b. 888888
888 888 "88b 888P" d88" 888 888 "88b "88b 888
888 888 .d888888 888 888 888 888 888 .d888888 888
888 888 888 888 888 Y88b 888 888 888 888 888 Y88b.
888 888 "Y888888 888 "Y88888 888 888 "Y888888 "Y888'
👷 Welcome to Hardhat v2.9.9 👷
? What do you want to do? …
Create a JavaScript project
❯ Create a TypeScript project
Create an empty hardhat.config.js
Quit
Next, compile it by running:
npx hardhat compile
The compiled artifacts will be saved in the artifacts/ directory by default. This directory also contains debug symbols that the solidity debugger relies upon.
Debugging Your Hardhat Project
Open your project folder in Visual Studio Code, then access the Solidity Debugger menu from the left sidebar. Click on New Target and then on Create from currently-open Hardhat project. Click Next and then Create.
A Solidity Debugger Test will be created and opened:
The solidity debugger is not capable of debugging contracts directly, therefore, it uses .t.sol files as entry points.
In this case, Test1.t.sol serves as the debugger's entry point. If we put a breakpoint on line 8 in Test1.t.sol and then press the Debug button we will be able to debug the test contract. However, we are actually interested in debugging Lock.sol.
One way to go about it is to import Lock.sol from Test1.t.sol and then deploy the Lock contract from inside the test.
To do this, replace the content of Test1.t.sol with the following code:
// SPDX-License-Identifier: MIT
pragma solidity >= 0.4.21 < 0.9.0;
import "contracts/Lock.sol";
contract DbgEntry {
constructor() {
Lock lock = new Lock(block.timestamp + 100);
}
}
Put a breakpoint on Lock lock = new Lock(block.timestamp + 100); and click the Debug button. When the breakpoint hits, use Step Into to watch Lock.sol constructor in action:
Depositing ETH into the Lock Contract
The lock contract is meant to receive ETH and to hold it until a future time. The owner could then call withdraw() to get the ETH back from the contract. Therefore the first step in testing Lock.sol is to deposit ETH into the contract:
// SPDX-License-Identifier: MIT
pragma solidity >= 0.4.21 < 0.9.0;
import "contracts/Lock.sol";
contract DbgEntry {
event EvmSetBalance(address, uint256);
constructor() {
emit EvmSetBalance(address(this), 1000);
Lock lock = new Lock{value: 100}(block.timestamp + 100);
}
}
EvmSetBalance is a cheat-code that instructs the debugger to change the balance of an address. We use it to change our own balance to 1000, so that we would have enough balance to make a deposit. We then deploy Lock with an initial deposit of 100.
Set a breakpoint on the line where a new Lock is created, then click Debug and use Step Into to watch the lock being initialized with a value of 100.
Withdrawing ETH from the Lock Contract
The owner of the contract can call withdraw() to get the money back. However, if we simply try calling withdraw() from our Test, our transaction would get reverted because Lock's unlock time is set to a time in the future.
Therefore, if we want withdraw() to succeed during our test, we need to somehow move block.timestamp into the future. We can do that using the EvmSetBlockTimestamp cheat code:
// SPDX-License-Identifier: MIT
pragma solidity >= 0.4.21 < 0.9.0;
import "contracts/Lock.sol";
contract DbgEntry {
event EvmSetBalance(address, uint256);
event EvmSetBlockTimestamp(uint256 newTimeStamp);
event EvmPrint(string);
event EvmPrint(address);
event EvmPrint(uint256);
constructor() {
emit EvmSetBalance(address(this), 1000);
Lock lock = new Lock{value: 100}(block.timestamp + 100);
emit EvmSetBlockTimestamp(block.timestamp + 101);
emit EvmPrint("Balance before:");
emit EvmPrint(address(this).balance);
lock.withdraw();
emit EvmPrint("Balance after:");
emit EvmPrint(address(this).balance);
}
}
Now that block.timestamp is set past the unlock time, we can step into withdraw() and observe its successful completion.