TechLead
Lesson 10 of 20
5 min read
Web3 & Blockchain

Hardhat Development Environment

Set up a professional Solidity development environment with Hardhat for compiling, deploying, testing, and debugging smart contracts

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

Continue Learning