Hardhat
Used in the two older contract repos, and alongside Foundry elsewhere for scripting and verification.
For new contract repos, prefer Foundry — see ../ekx-foundry/SKILL.md.
Hardhat stays where it already is, and where we need its JS ecosystem
(Ignition, TypeChain typings consumed directly by a Next.js frontend).
Docs: https://hardhat.org/docs
Config
A config trimmed to the shape we use:
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
const config: HardhatUserConfig = {
solidity: {
version: "0.8.24",
settings: { optimizer: { enabled: true, runs: 200 } },
},
networks: {
base: { url: process.env.BASE_MAINNET_RPC_URL || "https://mainnet.base.org", chainId: 8453, accounts: [process.env.DEPLOYER_PRIVATE_KEY!] },
baseSepolia: { url: process.env.BASE_SEPOLIA_RPC_URL || "https://sepolia.base.org", chainId: 84532, accounts: [process.env.DEPLOYER_PRIVATE_KEY!] },
localhost: { url: "http://127.0.0.1:8545", chainId: 1337 },
},
etherscan: { apiKey: { base: process.env.BASESCAN_API_KEY!, baseSepolia: process.env.BASESCAN_API_KEY! } },
gasReporter: { enabled: process.env.REPORT_GAS === "true", currency: "USD" },
};
export default config;
Guard the accounts array — [undefined!] crashes the whole config load, so every
command fails even ones that need no key. Prefer
accounts: process.env.DEPLOYER_PRIVATE_KEY ? [process.env.DEPLOYER_PRIVATE_KEY] : [].
The toolbox
@nomicfoundation/hardhat-toolbox bundles what all five repos install anyway:
hardhat-ethers, chai-matchers, network-helpers, verify, TypeChain, gas-reporter,
solidity-coverage.
Commands
npx hardhat compile
npx hardhat test
npx hardhat test --grep "bid"
REPORT_GAS=true npx hardhat test
npx hardhat coverage
npx hardhat node # local chain
npx hardhat run scripts/deploy.ts --network baseSepolia
npx hardhat verify --network baseSepolia <ADDR> "arg1" "arg2"
Tests
import { expect } from "chai";
import { ethers } from "hardhat";
import { loadFixture, time } from "@nomicfoundation/hardhat-network-helpers";
describe("DogRental", () => {
async function deploy() {
const [owner, renter] = await ethers.getSigners();
const usdc = await (await ethers.getContractFactory("MockUSDC")).deploy();
const rental = await (await ethers.getContractFactory("DogRental")).deploy(usdc.target);
return { rental, usdc, owner, renter };
}
it("reverts when payment is short", async () => {
const { rental, renter } = await loadFixture(deploy);
await expect(rental.connect(renter).book(1, 1_000_000n))
.to.be.revertedWithCustomError(rental, "InsufficientPayment");
});
});
loadFixture snapshots and rewinds state — far faster than redeploying per test.
time.increase(86400) for time travel.
ethers v6 note: it is contract.target, not contract.address. Values are
bigint, not BigNumber — 1_000_000n, and no .mul()/.add(). Every one of our
repos is on v6; old tutorials showing v5 will not run.
TypeChain → frontend
Generate typings the frontend imports directly:
typechain: { outDir: "typechain-types", target: "ethers-v6" }
Regenerate on every ABI change (npx hardhat compile does it) or the frontend types
lie about the contract.
Ignition
// ignition/modules/Auction.ts
export default buildModule("Auction", (m) => {
const auction = m.contract("Auction", [m.getParameter("usdc")]);
return { auction };
});
npx hardhat ignition deploy ignition/modules/Auction.ts --network baseSepolia --verify
Ignition is resumable — a failed multi-contract deploy picks up where it stopped instead of redeploying everything. That is its one real advantage over a plain script.
Gotchas
- ethers v6 API break —
.target, bigints, no BigNumber math. accounts: [undefined]breaks every command. Guard it.MockUSDCis for local nodes only. Base Sepolia has real Circle USDC at0x036CbD53842c5426634e7929541eC2318f3dcF7e— use it, and the faucet at https://faucet.circle.com.- Verification needs exact constructor args in the same order and encoding.
hardhat-toolboxpins peer deps hard. Upgrading one plugin alone usually breaks the install; upgrade the toolbox as a unit.
Migrating to Foundry?
Worth it when the repo is contract-heavy and test-heavy (fuzzing, invariants, fork
tests are all better in Foundry). Not worth it for a repo whose value is in the
Ignition deployment graph and the TypeChain types the frontend consumes.
@nomicfoundation/hardhat-foundry lets both coexist.