What is Hardhat?
Hardhat is the most popular Ethereum development environment. It provides a local blockchain for testing, a Solidity compiler, a test runner, a debugger, and a plugin ecosystem. Hardhat makes the entire smart contract development lifecycle — from writing code to deploying on mainnet — smooth and productive.
Why Hardhat?
- Hardhat Network: A local Ethereum network with instant mining, console.log in Solidity, and detailed stack traces
- TypeScript support: First-class TypeScript integration for type-safe scripts and tests
- Plugin ecosystem: Ethers.js integration, gas reporter, contract verification, and more
- Forking: Fork mainnet state to test against real deployed contracts locally
- Fast compilation: Incremental compilation and caching for quick feedback loops
Project Setup
// Terminal commands to set up a new Hardhat project
// mkdir my-project && cd my-project
// npm init -y
// npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox
// npx hardhat init
// -> Select "Create a TypeScript project"
// Project structure:
// my-project/
// contracts/ — Solidity contracts
// test/ — Test files
// scripts/ — Deployment scripts
// hardhat.config.ts — Configuration
// .env — Environment variables (API keys, private keys)
// hardhat.config.ts — Full configuration
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
import * as dotenv from "dotenv";
dotenv.config();
const config: HardhatUserConfig = {
solidity: {
version: "0.8.24",
settings: {
optimizer: {
enabled: true,
runs: 200,
},
viaIR: true,
},
},
networks: {
hardhat: {
// Local network settings
chainId: 31337,
// Fork mainnet for testing against real contracts
// forking: {
// url: process.env.MAINNET_RPC_URL!,
// blockNumber: 19000000,
// },
},
sepolia: {
url: process.env.SEPOLIA_RPC_URL || "",
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
},
mainnet: {
url: process.env.MAINNET_RPC_URL || "",
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
},
},
etherscan: {
apiKey: process.env.ETHERSCAN_API_KEY,
},
gasReporter: {
enabled: true,
currency: "USD",
coinmarketcap: process.env.COINMARKETCAP_API_KEY,
},
};
export default config;
Writing and Compiling Contracts
// contracts/Vault.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
// Hardhat console for debugging (remove before deployment!)
import "hardhat/console.sol";
contract Vault is Ownable, ReentrancyGuard {
mapping(address => uint256) public balances;
uint256 public totalDeposits;
event Deposited(address indexed user, uint256 amount);
event Withdrawn(address indexed user, uint256 amount);
constructor() Ownable(msg.sender) {}
function deposit() external payable {
require(msg.value > 0, "Must send ETH");
// console.log in Solidity! (Hardhat only)
console.log("Deposit from %s: %s wei", msg.sender, msg.value);
balances[msg.sender] += msg.value;
totalDeposits += msg.value;
emit Deposited(msg.sender, msg.value);
}
function withdraw(uint256 amount) external nonReentrant {
require(balances[msg.sender] >= amount, "Insufficient balance");
console.log("Withdraw by %s: %s wei", msg.sender, amount);
balances[msg.sender] -= amount;
totalDeposits -= amount;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
emit Withdrawn(msg.sender, amount);
}
}
// Compile: npx hardhat compile
Deployment Scripts
// scripts/deploy.ts
import { ethers } from "hardhat";
async function main() {
const [deployer] = await ethers.getSigners();
console.log("Deploying with:", deployer.address);
console.log("Balance:", ethers.formatEther(
await ethers.provider.getBalance(deployer.address)
), "ETH");
// Deploy the contract
const Vault = await ethers.getContractFactory("Vault");
const vault = await Vault.deploy();
await vault.waitForDeployment();
const address = await vault.getAddress();
console.log("Vault deployed to:", address);
// Verify on Etherscan (for testnets/mainnet)
if (network.name !== "hardhat" && network.name !== "localhost") {
console.log("Waiting for block confirmations...");
await vault.deploymentTransaction()?.wait(5);
await hre.run("verify:verify", {
address: address,
constructorArguments: [],
});
console.log("Contract verified on Etherscan");
}
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
// Run: npx hardhat run scripts/deploy.ts --network sepolia
Hardhat Tasks and Console
// Custom Hardhat task in hardhat.config.ts
import { task } from "hardhat/config";
task("balance", "Prints an account's balance")
.addParam("account", "The account's address")
.setAction(async (taskArgs, hre) => {
const balance = await hre.ethers.provider.getBalance(taskArgs.account);
console.log(hre.ethers.formatEther(balance), "ETH");
});
// Run: npx hardhat balance --account 0x123...
// Interactive console
// npx hardhat console --network localhost
// > const Vault = await ethers.getContractFactory("Vault")
// > const vault = await Vault.deploy()
// > await vault.deposit({ value: ethers.parseEther("1.0") })
// > (await vault.totalDeposits()).toString()
Mainnet Forking
One of Hardhat's most powerful features is mainnet forking. It creates a local copy of the Ethereum mainnet state at a specific block number, allowing you to test your contracts against real deployed protocols like Uniswap, Aave, or any other contract — without spending real ETH.
// hardhat.config.ts — Enable forking
networks: {
hardhat: {
forking: {
url: "https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY",
blockNumber: 19500000, // Pin to specific block for reproducibility
},
},
}
// Now you can interact with mainnet contracts locally
// scripts/fork-test.ts
import { ethers } from "hardhat";
async function main() {
// Interact with the real USDC contract on your local fork
const USDC = await ethers.getContractAt(
"IERC20",
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
);
const totalSupply = await USDC.totalSupply();
console.log("USDC Total Supply:", ethers.formatUnits(totalSupply, 6));
// Impersonate a whale account
const whale = "0x47ac0Fb4F2D84898e4D9E7b4DaB3C24507a6D503";
await ethers.provider.send("hardhat_impersonateAccount", [whale]);
const whaleSigner = await ethers.getSigner(whale);
// Transfer from whale — works on the local fork!
const [deployer] = await ethers.getSigners();
await USDC.connect(whaleSigner).transfer(
deployer.address,
ethers.parseUnits("10000", 6)
);
console.log("Received 10,000 USDC from whale");
}
Essential Hardhat Commands
- npx hardhat compile: Compile all contracts in the contracts/ directory
- npx hardhat test: Run all tests with Mocha + Chai
- npx hardhat node: Start a local JSON-RPC node for development
- npx hardhat run scripts/deploy.ts: Execute a deployment script
- npx hardhat verify: Verify contract source code on Etherscan
- npx hardhat clean: Clear compilation cache and artifacts