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.
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
mkdir my-project && cd my-projectnpm init -ynpm install --save-dev hardhatnpx hardhat initcontracts/, 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.
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
contracts/*.solsolc via Hardhatartifacts/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.
$ 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.
// 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
deployFixture() — deploy token, get walletstoken.write.transfer([alice, 100n])expect(balance).to.equal(100n)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.
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
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.
// 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
- Project init: Initialize a Hardhat TypeScript project. Copy the
MyToken.solfrom Module 9 intocontracts/. Runnpx hardhat compileand verify it compiles successfully with no errors. - 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.
- Deploy with Ignition: Create an Ignition module for MyToken. Deploy to Hardhat Network using
npx hardhat ignition deploy. Verify the deployed contract returns the correctname(). - Debug with console.log: Add
console.logstatements to thetransfer()function of your token. Run your tests and observe the debug output in the terminal. - 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
- What does
npx hardhat compileproduce and where are the outputs stored? - What is
loadFixtureand why is it better than deploying inbeforeEach? - What is Hardhat Ignition and how does it differ from traditional deployment scripts?
- Why should you never commit private keys to git? What alternatives exist for production?
- 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.