← Course home Module 10 / 19 Blockchain — Part 2

Hardhat v3: The Development Environment

You can write Solidity. Now you need a professional environment to compile, test, deploy, and debug your contracts. Hardhat v3 is the industry standard — used by Uniswap, Aave, OpenSea, and thousands of production projects.

~75 min read Module 10 ⟠ Part 2

Recap: From Code to Tooling

Modules 7-9 taught you Solidity — types, control flow, advanced patterns, and ERC-20 tokens. But writing .sol files in a text editor isn't enough. You need: compilation, testing, deployment, debugging, and a local blockchain. Hardhat provides all of this.

🧱

What You've Written

Solidity contracts: TodoList, Vault, ERC-20 token. But so far you have no way to compile, deploy, or test them outside of Remix. You need a real development environment.

🔨

What You Need

A professional dev environment: compiler integration, a local blockchain, a test runner, deployment scripts, and a console debugger. Hardhat v3 does it all in one tool.

💡

Why Hardhat?

Hardhat is the most popular Ethereum development framework. TypeScript-first. Built-in local network (Hardhat Network). Extensible via plugins. Used by >70% of Ethereum projects. Alternatives: Foundry (Rust-based, faster compilation but steeper learning curve), Remix (browser-only, limited for production). Hardhat strikes the best balance between power and accessibility.

Setting Up a Hardhat Project

From zero to a running project in 5 minutes. Hardhat's scaffolding gives you everything you need to start writing, compiling, and testing contracts immediately.

Step-by-Step Setup

Step
Command
What It Does
1
mkdir my-project && cd my-project
Create and enter your project directory.
2
npm init -y
Initialize a Node.js project with default package.json.
3
npm install --save-dev hardhat
Install Hardhat as a dev dependency.
4
npx hardhat init
Choose "Create a TypeScript project". Installs plugins automatically.
5
Project structure appears
contracts/, test/, scripts/, hardhat.config.ts are created.
📁

Project Structure

my-project/
├── contracts/          # Your .sol files
│   └── Lock.sol        # Example contract (auto-generated)
├── test/               # Test files
│   └── Lock.ts         # Example test
├── scripts/            # Deployment scripts
│   └── deploy.ts       # Example deployment
├── hardhat.config.ts   # Hardhat configuration
├── package.json
└── tsconfig.json

⚙️

hardhat.config.ts

The brain of your project. Defines the Solidity compiler version, network configurations (localhost, testnets, mainnet), and plugins. The default config compiles with solc 0.8.20 and enables Hardhat Network automatically.

hardhat.config.ts TypeScript
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox-viem";

const config: HardhatUserConfig = {
  solidity: "0.8.20",
};

export default config;

Compiling Contracts

Hardhat wraps the solc compiler. One command compiles every contract in your project: npx hardhat compile.

Compilation Pipeline

Stage
Location
Description
Input
contracts/*.sol
Your Solidity source files.
Compiler
solc via Hardhat
Hardhat downloads and manages the correct solc version automatically.
Output
artifacts/
ABI (Application Binary Interface) + bytecode for each contract. Also generates type-safe bindings in typechain-types/ if using TypeChain.
📦

Key Details

Compiles all .sol files in contracts/ recursively. Generates artifacts in artifacts/ (ABI + bytecode). Type-safe bindings in typechain-types/ (if using TypeChain plugin). Error output is human-readable with exact source locations — no more cryptic compiler errors.

🔀

Multiple Solidity Versions

You can compile contracts with different solc versions in the same project. Add multiple compiler entries in hardhat.config.ts. This is useful when importing OpenZeppelin contracts that may target a different version than your own code.

Terminal Shell
$ npx hardhat compile
Compiling 1 Solidity file
Generating typings for: 1 artifacts in dir: typechain-types
Successfully generated 6 typings!
Compilation finished successfully

Hardhat Network: Your Local Blockchain

Hardhat includes a built-in local Ethereum node. No Docker, no external tools. It runs in-process, starts instantly, and gives you everything you need to develop and test smart contracts.

Instant Mining

Blocks are mined instantly when you send a transaction. No waiting for block confirmation. Perfect for testing — your test suite runs in seconds, not minutes.

👛

20 Pre-funded Accounts

Each account starts with 10,000 ETH. Known private keys for testing. Never use these accounts on mainnet — their private keys are publicly known.

🖨️

Console.log in Solidity

import "hardhat/console.sol" in any contract, then use console.log("value:", myVar). Output appears in your terminal during tests and local deployment. Revolutionary for debugging — no more guessing what happened inside a transaction.

🔄

Forking Mainnet

npx hardhat node --fork https://mainnet.infura.io/v3/YOUR_KEY — clone mainnet state to your local machine. Test against real Uniswap, real USDT, real everything. Killer feature: simulate transactions against production state without spending real ETH.

🔍

Stack Traces

When a transaction reverts, Hardhat shows the full Solidity stack trace with line numbers. No more guessing which require() failed. This alone saves hours of debugging on every project.

MyContract.sol Solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "hardhat/console.sol";

contract MyContract {
    uint256 public value;

    function setValue(uint256 _value) external {
        console.log("Old value: %s", value);
        console.log("New value: %s", _value);
        console.log("Caller: %s", msg.sender);
        value = _value;
    }
}

// Terminal output during test:
// Old value: 0
// New value: 42
// Caller: 0xf39Fd6e51...

Testing Smart Contracts

Tests are non-negotiable in smart contract development. Bugs cost real money. Hardhat uses Mocha + Chai + viem (or ethers.js). Every function must be tested before deployment.

Test Structure

Phase
Purpose
Example
Fixture
Deploy contract + set initial state
deployFixture() — deploy token, get wallets
Action
Call the function being tested
token.write.transfer([alice, 100n])
Assert
Check balances, events, reverts
expect(balance).to.equal(100n)
Cleanup
Automatic via loadFixture snapshot
State resets between every test — no leaks
🧪

Key Patterns

describe/it blocks (Mocha structure). loadFixture for efficient test setup — deploy once, snapshot, reset between tests. expect().to.be.revertedWith() for error checking. expect().to.emit() for event verification. These patterns ensure your tests are fast, isolated, and thorough.

MyToken.test.ts TypeScript
import { loadFixture } from "@nomicfoundation/hardhat-toolbox-viem/network-helpers";
import { expect } from "chai";
import hre from "hardhat";

describe("MyToken", function () {
  async function deployFixture() {
    const [owner, alice, bob] = await hre.viem.getWalletClients();
    const token = await hre.viem.deployContract("MyToken", [
      "MyToken", "MTK", 1000000n
    ]);
    const publicClient = await hre.viem.getPublicClient();
    return { token, owner, alice, bob, publicClient };
  }

  it("should have correct name and symbol", async function () {
    const { token } = await loadFixture(deployFixture);
    expect(await token.read.name()).to.equal("MyToken");
    expect(await token.read.symbol()).to.equal("MTK");
  });

  it("should transfer tokens", async function () {
    const { token, owner, alice } = await loadFixture(deployFixture);
    await token.write.transfer([alice.account.address, 100n]);
    const balance = await token.read.balanceOf([alice.account.address]);
    expect(balance).to.equal(100n);
  });

  it("should revert on insufficient balance", async function () {
    const { token, alice, bob } = await loadFixture(deployFixture);
    await expect(
      token.write.transfer([bob.account.address, 100n],
        { account: alice.account })
    ).to.be.rejected;
  });
});
📊

Test Coverage

Run npx hardhat coverage (solidity-coverage plugin). Aim for >90% line coverage on critical contracts. Untested code = unknown bugs. Coverage reports show exactly which lines and branches have never been executed by your test suite.

Deploying Contracts

Hardhat v3 uses Ignition for declarative deployments. You define what to deploy, and Hardhat handles how — deployment order, transaction management, and artifact tracking.

🚀

Hardhat Ignition

Declarative deployment modules. Define the contract + constructor arguments. Hardhat manages deployment order, tracks deployed addresses, and handles upgrades. Replaces the old scripts/ pattern with a more robust, reproducible approach.

🌐

Networks

Deploy to: Hardhat Network (local, default), testnet (Sepolia, Goerli), or mainnet. Configure each network in hardhat.config.ts with an RPC URL and a private key.

ignition/modules/MyToken.ts TypeScript
// ignition/modules/MyToken.ts
import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";

const MyTokenModule = buildModule("MyTokenModule", (m) => {
  const token = m.contract("MyToken", ["MyToken", "MTK", 1_000_000n]);
  return { token };
});

export default MyTokenModule;

// Deploy command:
// npx hardhat ignition deploy ignition/modules/MyToken.ts --network localhost
📂

Deployment Artifacts

Ignition saves deployment data in ignition/deployments/. Contains deployed addresses, transaction hashes, and constructor arguments. Crucial for contract verification on Etherscan and for frontend integration — your dApp needs to know where the contract lives.

⚠️

Private Keys — DANGER

NEVER commit private keys to git. Use .env files + the dotenv package. Add .env to .gitignore. For production deployments: use hardware wallets or key management services (AWS KMS, HashiCorp Vault). A leaked private key means total loss of all funds controlled by that account.

Debugging & Advanced Features

When things go wrong (and they will), Hardhat helps you find the bug fast. These tools turn hours of frustration into minutes of targeted debugging.

🖨️

console.log

Import hardhat/console.sol in any .sol file. Works with uint, string, bool, address. Output appears in your terminal during tests or local deployment. Remove before mainnet deployment — console.log calls waste gas in production.

📍

Stack Traces

Full Solidity stack traces on revert. Shows the exact line number, function name, and revert reason. No more console.log debugging loops — you see exactly where and why the transaction failed.

💻

Hardhat Console

npx hardhat console --network localhost — an interactive REPL. Deploy contracts, call functions, inspect state — all from the terminal. Like the Node.js REPL, but connected to an Ethereum network.

🔄

Putting It All Together

The typical Hardhat workflow: 1. Write contract 2. Compile (npx hardhat compile) 3. Write tests 4. Run tests (npx hardhat test) 5. Fix bugs using console.log + stack traces 6. Deploy to local network 7. Verify on Etherscan 8. Deploy to testnet 9. Test on testnet 10. Deploy to mainnet.

MyToken.sol Solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "hardhat/console.sol";

contract MyToken {
    mapping(address => uint256) private _balances;

    function transfer(address to, uint256 amount) external {
        console.log("Sender: %s, Balance: %s", msg.sender, _balances[msg.sender]);
        console.log("Transfer amount: %s to %s", amount, to);
        require(_balances[msg.sender] >= amount, "Insufficient balance");
        _balances[msg.sender] -= amount;
        _balances[to] += amount;
        console.log("Transfer complete. New balances:");
        console.log("  Sender: %s", _balances[msg.sender]);
        console.log("  Recipient: %s", _balances[to]);
    }
}

Exercise & Self-Check

Exercises

  1. Project init: Initialize a Hardhat TypeScript project. Copy the MyToken.sol from Module 9 into contracts/. Run npx hardhat compile and verify it compiles successfully with no errors.
  2. Write 5 tests: Using the test file structure from this module, write tests for MyToken: (1) name check, (2) transfer, (3) approve + transferFrom, (4) mint (if minter role exists), (5) revert on insufficient balance.
  3. Deploy with Ignition: Create an Ignition module for MyToken. Deploy to Hardhat Network using npx hardhat ignition deploy. Verify the deployed contract returns the correct name().
  4. Debug with console.log: Add console.log statements to the transfer() function of your token. Run your tests and observe the debug output in the terminal.
  5. Fork mainnet: Start a Hardhat node with mainnet forking enabled. Write a script that reads the totalSupply() of USDT (0xdAC17F958D2ee523a2206206994597C13D831ec7) from your local fork.

Self-Check Questions

  1. What does npx hardhat compile produce and where are the outputs stored?
  2. What is loadFixture and why is it better than deploying in beforeEach?
  3. What is Hardhat Ignition and how does it differ from traditional deployment scripts?
  4. Why should you never commit private keys to git? What alternatives exist for production?
  5. How does mainnet forking work and what can you test with it?
🎯

Module Summary

You now have a complete professional development environment. You can compile Solidity contracts with npx hardhat compile, test them with Mocha/Chai/viem, deploy them with Hardhat Ignition, and debug them with console.log and stack traces. Hardhat Network gives you a local blockchain with instant mining, pre-funded accounts, and mainnet forking.

Next module

You have the tools. Now it's time to master testing. Module 11 dives deep into smart contract testing — unit tests, integration tests, fuzzing, and coverage analysis.

Module 11: Testing Contracts →
📋 Course Project

Ready to apply everything you've learned? The end-of-course project combines smart contracts, AI, and Node.js into a real decentralised academic assistant.

View Project Brief →