Skip to main content
Technology & EngineeringWeb3 Development302 lines

Hardhat

Hardhat development environment for compiling, testing, deploying, and debugging Solidity smart contracts

Quick Summary35 lines
You are an expert in Hardhat for building, testing, and deploying Solidity smart contracts in professional development workflows.

## Key Points

*   **Console.log Left in Production Code.** Forgetting to remove Hardhat's `console.log` imports from contracts before deployment wastes gas and may leak internal state information.
*   **Hardcoded Network Configuration.** Embedding RPC URLs and private keys directly in `hardhat.config.ts` instead of using environment variables exposes credentials in version control.
*   **No Task Automation for Deployment.** Performing multi-step deployments manually instead of scripting them as Hardhat tasks creates non-reproducible, error-prone deployment processes.
1. **Use `loadFixture`** for test setup. It snapshots and reverts the blockchain state between tests, making tests fast and isolated.
2. **Pin fork block numbers** in `hardhat.config.ts` for deterministic tests. Unpinned forks produce flaky results as mainnet state changes.
3. **Use Hardhat Ignition** for deployment instead of raw scripts. It handles idempotent deployments, dependency ordering, and resumability.
4. **Enable the gas reporter** during development to catch gas regressions early.
5. **Use `console.log` in Solidity** (`import "hardhat/console.sol"`) for debugging. Remove before deploying to production.
6. **Write both positive and negative test cases.** Test that functions revert correctly with custom errors, not just that they succeed.
7. **Configure the optimizer** with a `runs` value that matches expected usage: low runs for rarely called contracts, high runs for frequently called ones.
- **Not resetting fork state between tests.** Without `loadFixture`, tests share state and produce order-dependent results.
- **Leaving `console.log` imports in production code.** This increases deployment gas cost. Use a linter rule or pre-deploy script to catch it.

## Quick Example

```bash
mkdir my-project && cd my-project
npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox
npx hardhat init
```

```bash
# Deploy
npx hardhat ignition deploy ignition/modules/Vault.ts --network sepolia

# Verify on Etherscan
npx hardhat verify --network sepolia DEPLOYED_ADDRESS constructor_arg1
```
skilldb get web3-development-skills/HardhatFull skill: 302 lines
Paste into your CLAUDE.md or agent config

Hardhat — Web3 Development

You are an expert in Hardhat for building, testing, and deploying Solidity smart contracts in professional development workflows.

Overview

Hardhat is a development environment for Ethereum that provides a local blockchain node, a testing framework, a task runner, and a plugin ecosystem. It supports Solidity compilation, automated testing with Mocha and Chai, deployment scripting, contract verification, and integrated debugging with stack traces and console.log support in Solidity.

Core Philosophy

Hardhat provides a JavaScript/TypeScript-first development experience that leverages the npm ecosystem for extensibility through plugins. Its local Hardhat Network simulates a full EVM with features like Solidity stack traces, console.log debugging, and mainnet forking, making the development-test-debug cycle as fast as possible. The plugin architecture allows teams to compose exactly the toolchain they need while maintaining reproducible builds and deployments.

Anti-Patterns

  • Unversioned Compiler and Plugin Dependencies. Using latest or unpinned versions for Solidity compiler or Hardhat plugins causes non-reproducible builds where the same source code produces different bytecode across environments.

  • Console.log Left in Production Code. Forgetting to remove Hardhat's console.log imports from contracts before deployment wastes gas and may leak internal state information.

  • Testing Only Against Hardhat Network. Running all tests exclusively on the local Hardhat Network without fork testing against mainnet state misses integration issues with deployed protocol contracts.

  • Hardcoded Network Configuration. Embedding RPC URLs and private keys directly in hardhat.config.ts instead of using environment variables exposes credentials in version control.

  • No Task Automation for Deployment. Performing multi-step deployments manually instead of scripting them as Hardhat tasks creates non-reproducible, error-prone deployment processes.

Core Concepts

Project Setup

mkdir my-project && cd my-project
npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox
npx hardhat init

Configuration

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

const config: HardhatUserConfig = {
  solidity: {
    version: "0.8.24",
    settings: {
      optimizer: {
        enabled: true,
        runs: 200,
      },
      viaIR: true, // Enable IR-based compilation for complex contracts
    },
  },
  networks: {
    hardhat: {
      forking: {
        url: `https://eth-mainnet.g.alchemy.com/v2/${process.env.ALCHEMY_KEY}`,
        blockNumber: 19000000, // Pin to a specific block for deterministic tests
      },
    },
    sepolia: {
      url: `https://eth-sepolia.g.alchemy.com/v2/${process.env.ALCHEMY_KEY}`,
      accounts: [process.env.DEPLOYER_PRIVATE_KEY!],
    },
    mainnet: {
      url: `https://eth-mainnet.g.alchemy.com/v2/${process.env.ALCHEMY_KEY}`,
      accounts: [process.env.DEPLOYER_PRIVATE_KEY!],
    },
  },
  etherscan: {
    apiKey: process.env.ETHERSCAN_API_KEY,
  },
  gasReporter: {
    enabled: true,
    currency: "USD",
  },
};

export default config;

Writing Tests

// test/Vault.test.ts
import { expect } from "chai";
import { ethers } from "hardhat";
import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers";

describe("Vault", function () {
  async function deployVaultFixture() {
    const [owner, user1, user2] = await ethers.getSigners();
    const Vault = await ethers.getContractFactory("Vault");
    const vault = await Vault.deploy();
    return { vault, owner, user1, user2 };
  }

  describe("Deposits", function () {
    it("should accept deposits and update balance", async function () {
      const { vault, user1 } = await loadFixture(deployVaultFixture);

      await vault.connect(user1).deposit({ value: ethers.parseEther("1.0") });

      expect(await vault.balances(user1.address)).to.equal(
        ethers.parseEther("1.0")
      );
    });

    it("should emit Deposit event", async function () {
      const { vault, user1 } = await loadFixture(deployVaultFixture);

      await expect(
        vault.connect(user1).deposit({ value: ethers.parseEther("1.0") })
      )
        .to.emit(vault, "Deposit")
        .withArgs(user1.address, ethers.parseEther("1.0"));
    });

    it("should revert on zero deposit", async function () {
      const { vault, user1 } = await loadFixture(deployVaultFixture);

      await expect(
        vault.connect(user1).deposit({ value: 0 })
      ).to.be.revertedWithCustomError(vault, "ZeroAmount");
    });
  });

  describe("Withdrawals", function () {
    it("should revert if insufficient balance", async function () {
      const { vault, user1 } = await loadFixture(deployVaultFixture);

      await expect(
        vault.connect(user1).withdraw(ethers.parseEther("1.0"))
      ).to.be.revertedWithCustomError(vault, "InsufficientBalance");
    });

    it("should transfer correct amount", async function () {
      const { vault, user1 } = await loadFixture(deployVaultFixture);

      await vault.connect(user1).deposit({ value: ethers.parseEther("2.0") });

      await expect(
        vault.connect(user1).withdraw(ethers.parseEther("1.0"))
      ).to.changeEtherBalances(
        [user1, vault],
        [ethers.parseEther("1.0"), ethers.parseEther("-1.0")]
      );
    });
  });
});

Time and Block Manipulation

import { time, mine } from "@nomicfoundation/hardhat-toolbox/network-helpers";

it("should unlock after time lock expires", async function () {
  const { vault, user1 } = await loadFixture(deployVaultFixture);

  await vault.connect(user1).deposit({ value: ethers.parseEther("1.0") });

  // Advance time by 7 days
  await time.increase(7 * 24 * 60 * 60);

  // Or set to a specific timestamp
  await time.increaseTo((await time.latest()) + 3600);

  // Mine a specific number of blocks
  await mine(10);

  // Now withdrawal should succeed
  await vault.connect(user1).withdraw(ethers.parseEther("1.0"));
});

Deployment Scripts with Hardhat Ignition

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

const VaultModule = buildModule("VaultModule", (m) => {
  const initialFee = m.getParameter("initialFee", 100); // basis points

  const vault = m.contract("Vault", [initialFee]);

  // Deploy dependent contracts
  const token = m.contract("RewardToken", []);

  // Call setup functions after deployment
  m.call(vault, "setRewardToken", [token]);

  return { vault, token };
});

export default VaultModule;
# Deploy
npx hardhat ignition deploy ignition/modules/Vault.ts --network sepolia

# Verify on Etherscan
npx hardhat verify --network sepolia DEPLOYED_ADDRESS constructor_arg1

Implementation Patterns

Mainnet Fork Testing

describe("Uniswap Integration", function () {
  it("should swap tokens on mainnet fork", async function () {
    // Impersonate a whale address
    const whale = await ethers.getImpersonatedSigner("0xWhaleAddress...");

    // Fund the impersonated account with ETH for gas
    await ethers.provider.send("hardhat_setBalance", [
      "0xWhaleAddress...",
      "0x56BC75E2D63100000", // 100 ETH
    ]);

    const usdc = await ethers.getContractAt("IERC20", USDC_ADDRESS);
    const router = await ethers.getContractAt("ISwapRouter", UNISWAP_ROUTER);

    const balanceBefore = await usdc.balanceOf(whale.address);

    await usdc.connect(whale).approve(UNISWAP_ROUTER, ethers.MaxUint256);
    await router.connect(whale).exactInputSingle({
      tokenIn: USDC_ADDRESS,
      tokenOut: WETH_ADDRESS,
      fee: 3000,
      recipient: whale.address,
      amountIn: 1000_000000n, // 1000 USDC
      amountOutMinimum: 0,
      sqrtPriceLimitX96: 0,
    });

    const balanceAfter = await usdc.balanceOf(whale.address);
    expect(balanceBefore - balanceAfter).to.equal(1000_000000n);
  });
});

Custom Hardhat Tasks

// tasks/accounts.ts
import { task } from "hardhat/config";

task("balances", "Prints account balances")
  .addOptionalParam("network", "Network name", "hardhat")
  .setAction(async (taskArgs, hre) => {
    const accounts = await hre.ethers.getSigners();
    for (const account of accounts) {
      const balance = await hre.ethers.provider.getBalance(account.address);
      console.log(`${account.address}: ${hre.ethers.formatEther(balance)} ETH`);
    }
  });

Gas Optimization Testing

describe("Gas benchmarks", function () {
  it("should use less than 100k gas for transfer", async function () {
    const { token, user1, user2 } = await loadFixture(deployFixture);

    const tx = await token.connect(user1).transfer(user2.address, 1000n);
    const receipt = await tx.wait();

    expect(receipt!.gasUsed).to.be.lessThan(100_000n);
  });
});

Best Practices

  1. Use loadFixture for test setup. It snapshots and reverts the blockchain state between tests, making tests fast and isolated.
  2. Pin fork block numbers in hardhat.config.ts for deterministic tests. Unpinned forks produce flaky results as mainnet state changes.
  3. Use Hardhat Ignition for deployment instead of raw scripts. It handles idempotent deployments, dependency ordering, and resumability.
  4. Enable the gas reporter during development to catch gas regressions early.
  5. Use console.log in Solidity (import "hardhat/console.sol") for debugging. Remove before deploying to production.
  6. Write both positive and negative test cases. Test that functions revert correctly with custom errors, not just that they succeed.
  7. Configure the optimizer with a runs value that matches expected usage: low runs for rarely called contracts, high runs for frequently called ones.

Common Pitfalls

  • Not resetting fork state between tests. Without loadFixture, tests share state and produce order-dependent results.
  • Leaving console.log imports in production code. This increases deployment gas cost. Use a linter rule or pre-deploy script to catch it.
  • Forgetting to fund impersonated signers. Impersonated accounts start with their real balance, which may be zero ETH for gas.
  • Using hardhat network for deployment. The default Hardhat network is ephemeral. Always specify a persistent network for real deployments.
  • Not verifying contracts on block explorers. Unverified contracts erode user trust. Verify immediately after deployment.
  • Ignoring compiler warnings. Solidity warnings often indicate real issues such as shadowed variables or unused returns.

Install this skill directly: skilldb add web3-development-skills

Get CLI access →

Related Skills

Account Abstraction

Account Abstraction (AA) fundamentally changes how users interact with EVM chains by enabling smart contract accounts. This skill teaches you to build dApps with ERC-4337 compatible smart accounts, facilitating features like gas sponsorship, batch transactions, and flexible authentication methods.

Web3 Development208L

Aptos Development

Develop dApps and smart contracts on the Aptos blockchain using the Move language, Aptos SDKs, and CLI tools. This skill covers building secure, scalable, and user-friendly web3 applications leveraging Aptos' high throughput and low latency.

Web3 Development246L

Avalanche Development

This skill covers building decentralized applications and smart contracts on the Avalanche network, including its C-Chain, X-Chain, P-Chain, and custom Subnets. Learn to interact with the platform using SDKs, deploy EVM-compatible contracts, and manage cross-chain asset flows.

Web3 Development250L

Base Development

Develop, deploy, and interact with smart contracts and dApps on Base, an Ethereum Layer 2 solution built on the OP Stack. Leverage its EVM compatibility for scalable and cost-efficient Web3 applications.

Web3 Development254L

Cosmos SDK

Master the Cosmos SDK for building custom, sovereign blockchains (app-chains) and decentralized applications with inter-blockchain communication (IBC). This skill covers module development, message handling, and client interactions for creating high-performance, interoperable chains tailored to specific use cases.

Web3 Development276L

Cosmwasm Contracts

Develop, test, and deploy secure smart contracts on Cosmos SDK blockchains using Rust and CosmWasm.

Web3 Development296L