Difficulty: Advanced
Hardhat is the most widely used Ethereum development environment, providing a comprehensive toolkit for compiling, testing, deploying, and debugging smart contracts. It offers a local Ethereum network (Hardhat Network) with advanced features like Solidity stack traces, console.log in contracts, mainnet forking, and time/block manipulation. Hardhat's plugin architecture makes it extensible with an ecosystem of essential plugins for verification, gas reporting, contract sizing, and more.
Setting up a Hardhat project begins with initializing a Node.js project and installing Hardhat with its essential dependencies. The core dependencies include hardhat itself, @nomicfoundation/hardhat-toolbox (which bundles ethers.js, Chai matchers, gas reporter, Solidity coverage, and hardhat-verify), and OpenZeppelin contracts. The project structure includes contracts/ for Solidity files, test/ for tests, scripts/ or ignition/ for deployment, and hardhat.config.ts at the root.
The hardhat.config.ts file is the central configuration point. It defines the Solidity compiler version and optimizer settings, network configurations, plugin settings, and path overrides. Compiler optimization runs control the tradeoff between deployment cost and execution cost: higher values (200+) optimize for frequent execution, while lower values optimize for cheaper deployment. The viaIR flag enables the Yul-based pipeline for more optimized bytecode.
Network configuration allows deploying to different chains. Each entry specifies an RPC URL, chain ID, accounts (private keys or HD wallet mnemonics), and gas parameters. For local development, Hardhat Network provides instant mining and unlimited faucet ETH. For testnet deployment, use Alchemy or Infura as RPC providers and configure accounts via environment variables in a .env file.
Hardhat's plugin ecosystem is extensive. Essential plugins include @nomicfoundation/hardhat-verify for Etherscan verification, hardhat-gas-reporter for gas reports, solidity-coverage for code coverage, hardhat-contract-sizer for checking the 24KB limit, and @openzeppelin/hardhat-upgrades for proxy management. Custom tasks let you create reusable CLI operations for common workflows.
// hardhat.config.ts
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
import "@openzeppelin/hardhat-upgrades";
import "hardhat-contract-sizer";
import * as dotenv from "dotenv";
dotenv.config();
const ALCHEMY_KEY = process.env.ALCHEMY_API_KEY || "";
const DEPLOYER_KEY = process.env.DEPLOYER_PRIVATE_KEY || "0x" + "0".repeat(64);
const ETHERSCAN_KEY = process.env.ETHERSCAN_API_KEY || "";
const config: HardhatUserConfig = {
solidity: {
version: "0.8.24",
settings: {
optimizer: {
enabled: true,
runs: 200,
},
viaIR: false,
evmVersion: "cancun",
},
},
networks: {
hardhat: {
chainId: 31337,
forking: {
url: `https://eth-mainnet.g.alchemy.com/v2/${ALCHEMY_KEY}`,
blockNumber: 19000000,
enabled: false,
},
},
sepolia: {
url: `https://eth-sepolia.g.alchemy.com/v2/${ALCHEMY_KEY}`,
chainId: 11155111,
accounts: [DEPLOYER_KEY],
},
mainnet: {
url: `https://eth-mainnet.g.alchemy.com/v2/${ALCHEMY_KEY}`,
chainId: 1,
accounts: [DEPLOYER_KEY],
},
arbitrumOne: {
url: "https://arb1.arbitrum.io/rpc",
chainId: 42161,
accounts: [DEPLOYER_KEY],
},
base: {
url: "https://mainnet.base.org",
chainId: 8453,
accounts: [DEPLOYER_KEY],
},
},
etherscan: {
apiKey: {
mainnet: ETHERSCAN_KEY,
sepolia: ETHERSCAN_KEY,
arbitrumOne: process.env.ARBISCAN_API_KEY || "",
},
},
gasReporter: {
enabled: process.env.REPORT_GAS === "true",
currency: "USD",
},
contractSizer: {
alphaSort: true,
runOnCompile: true,
strict: true,
},
};
export default config;
This configuration sets up Solidity compilation with optimizer, multiple network targets (local, testnets, mainnet, L2s), Etherscan verification, gas reporting, and contract size checking. Environment variables keep sensitive data out of version control.
# Initialize a new Hardhat TypeScript project
mkdir my-project && cd my-project
npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox
npm install --save-dev @openzeppelin/contracts hardhat-contract-sizer dotenv
npx hardhat init # Select TypeScript project
# Create .env (add to .gitignore!)
echo 'ALCHEMY_API_KEY=your_key' > .env
echo 'DEPLOYER_PRIVATE_KEY=your_key' >> .env
echo '.env' >> .gitignore
# Common development commands
npx hardhat compile # Compile contracts
npx hardhat test # Run all tests
npx hardhat test test/MyContract.test.ts # Run specific test
REPORT_GAS=true npx hardhat test # Tests with gas report
npx hardhat coverage # Code coverage
npx hardhat node # Start local node
npx hardhat node --fork https://eth-mainnet.g.alchemy.com/v2/KEY # Forked node
npx hardhat size-contracts # Check contract sizes
npx hardhat verify --network sepolia 0xADDR "arg1" "arg2" # Verify
These commands set up a complete Hardhat project and demonstrate the common development workflow: compile, test, coverage, local node, and verification.
// In hardhat.config.ts
import { task } from "hardhat/config";
task("balances", "Prints account ETH balances")
.setAction(async (_, { ethers }) => {
const accounts = await ethers.getSigners();
for (const account of accounts.slice(0, 5)) {
const balance = await ethers.provider.getBalance(account.address);
console.log(`${account.address}: ${ethers.formatEther(balance)} ETH`);
}
});
task("deploy-verify", "Deploy and verify in one step")
.addParam("contract", "Contract name")
.addOptionalParam("args", "Constructor args as JSON", "[]")
.setAction(async (taskArgs, { ethers, run }) => {
const args = JSON.parse(taskArgs.args);
const Factory = await ethers.getContractFactory(taskArgs.contract);
const contract = await Factory.deploy(...args);
await contract.waitForDeployment();
const addr = await contract.getAddress();
console.log(`Deployed to: ${addr}`);
await contract.deploymentTransaction()?.wait(5);
try {
await run("verify:verify", { address: addr, constructorArguments: args });
console.log("Verified!");
} catch (e) {
console.log("Verification failed:", e);
}
});
task("fork-test", "Test against mainnet fork")
.setAction(async (_, { ethers, network }) => {
await network.provider.request({
method: "hardhat_reset",
params: [{ forking: {
jsonRpcUrl: `https://eth-mainnet.g.alchemy.com/v2/${process.env.ALCHEMY_API_KEY}`,
blockNumber: 19000000,
}}],
});
const whale = "0xBE0eB53F46cd790Cd13851d5EFf43D12404d33E8";
await network.provider.request({
method: "hardhat_impersonateAccount",
params: [whale],
});
const signer = await ethers.getSigner(whale);
const balance = await ethers.provider.getBalance(whale);
console.log(`Whale: ${ethers.formatEther(balance)} ETH`);
});
// Usage:
// npx hardhat balances --network sepolia
// npx hardhat deploy-verify --contract MyToken --args '["Token","TKN"]' --network sepolia
Custom tasks extend Hardhat's CLI. The balances task shows ETH balances, deploy-verify combines deployment with Etherscan verification, and fork-test demonstrates mainnet forking with account impersonation.
Hardhat, Development Environment, Configuration, Networks, Plugins