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.