| name | hardhat |
| description | [Applies to: **/*.{js,jsx}] Ensure Hardhat projects follow modern best practices for configuration, testing, deployment, and security using TypeScript and Hardhat 3's Rust runtime. |
| source | cursor_mdc |
hardhat Best Practices
Hardhat 3 is the definitive standard for Ethereum development. These rules enforce a modern, secure, and efficient workflow leveraging its Rust runtime, TypeScript support, and robust testing tools.
1. Adopt Hardhat 3 Immediately
Migrate all projects to Hardhat 3. Its Rust-powered runtime offers superior performance, instant stack traces, and enhanced debugging. Hardhat 2 is deprecated for new development.
2. Standardized Project Configuration (hardhat.config.ts)
Always use TypeScript for your Hardhat configuration. Pin Solidity compiler versions, enable Solidity test conventions, and activate essential plugins.
✅ GOOD: hardhat.config.ts
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
import "@nomicfoundation/hardhat-ignition";
import "hardhat-gas-reporter";
import "solidity-coverage";
import "@nomicfoundation/hardhat-solhint";
const config: HardhatUserConfig = {
solidity: {
compilers: [
{ version: "0.8.20" },
{ version: "0.8.21" },
],
settings: {
optimizer: {
enabled: true,
runs: 200,
},
},
},
networks: {
hardhat: {
chainId: 31337,
},
sepolia: {
url: process.env.SEPOLIA_RPC_URL || "",
accounts: process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [],
},
},
paths: {
sources: "./contracts",
tests: "./test",
cache: "./cache",
artifacts: "./artifacts",
},
gasReporter: {
enabled: process.env.REPORT_GAS !== undefined,
currency: "USD",
coinmarketcap: process.env.COINMARKETCAP_API_KEY,
},
typechain: {
outDir: "typechain-types",
target: "ethers-v6",
},
etherscan: {
apiKey: process.env.ETHERSCAN_API_KEY,
},
};
export default config;
3. Opinionated Code Organization
Maintain a clean, predictable project structure. Use TypeScript for all non-Solidity code (scripts, tasks, tests).
✅ GOOD: Project Structure
.
├── contracts/
│ ├── MyContract.sol
│ └── MyContract.t.sol // Solidity unit tests
├── scripts/
│ └── deploy.ts // TypeScript deployment scripts
├── test/
│ └── MyContract.test.ts // TypeScript integration tests
├── hardhat.config.ts
├── package.json
├── tsconfig.json
└── README.md
❌ BAD: Mixing JavaScript and TypeScript, inconsistent test locations
.
├── contracts/
│ └── MyContract.sol
├── scripts/
│ └── deploy.js // JavaScript deployment script
├── test/
│ ├── MyContract.test.js // JavaScript tests
│ └── MyContract.sol.js // Solidity tests in JS? No.
├── hardhat.config.js // JavaScript config
└── ...
4. Comprehensive Testing Strategy
Combine fast Solidity unit tests for core logic with expressive TypeScript integration tests for full system validation.
- Solidity Unit Tests (
.t.sol): For fast, isolated checks of contract logic.
- Place these directly in
contracts/ with a .t.sol extension.
- Avoid
testFail prefix; use vm.expectRevert for clarity.
- Hardhat's Solidity tests are Foundry-compatible but have differences (no inline NatSpec, no scripting cheatcodes like
startBroadcast).
- TypeScript Integration Tests (
.test.ts): For end-to-end scenarios, contract interactions, and network state changes.
- Use
ethers.js (via @nomicfoundation/hardhat-ethers) and chai-matchers.
- Leverage Hardhat Network Helpers for advanced scenarios.
✅ GOOD: Solidity Unit Test (contracts/MyContract.t.sol)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol"; // Use forge-std for cheatcodes
contract MyContractTest is Test {
MyContract public myContract;
function setUp() public {
myContract = new MyContract();
}
function test_InitialGreeting() public {
assertEq(myContract.greeting(), "Hello, Hardhat!");
}
function test_SetGreeting() public {
myContract.setGreeting("Hola!");
assertEq(myContract.greeting(), "Hola!");
}
function testRevert_SetGreetingEmpty() public {
vm.expectRevert("Greeting cannot be empty");
myContract.setGreeting("");
}
}
contract MyContract {
string public greeting;
constructor() {
greeting = "Hello, Hardhat!";
}
function setGreeting(string memory _greeting) public {
require(bytes(_greeting).length > 0, "Greeting cannot be empty");
greeting = _greeting;
}
}
✅ GOOD: TypeScript Integration Test (test/MyContract.test.ts)
import { expect } from "chai";
import { ethers } from "hardhat";
import { MyContract } from "../typechain-types";
describe("MyContract (Integration)", function () {
let myContract: MyContract;
beforeEach(async function () {
const MyContractFactory = await ethers.getContractFactory("MyContract");
myContract = await MyContractFactory.deploy();
await myContract.waitForDeployment();
});
it("should return the initial greeting", async function () {
expect(await myContract.greeting()).to.equal("Hello, Hardhat!");
});
it("should allow setting a new greeting", async function () {
const newGreeting = "Bonjour!";
await myContract.(newGreeting);
( myContract.())..(newGreeting);
});
(, () {
(myContract.())...();
});
});
5. Secure Deployment with Hardhat Ignition
Use Hardhat Ignition for declarative, reliable, and secure deployments. It handles complex dependencies and ensures atomic deployments.
✅ GOOD: Ignition Deployment (ignition/modules/MyContract.ts)
import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";
const MyContractModule = buildModule("MyContractModule", (m) => {
const myContract = m.contract("MyContract");
return { myContract };
});
export default MyContractModule;
❌ BAD: Manual, imperative deployment scripts prone to errors
import { ethers } from "hardhat";
async function main() {
const MyContract = await ethers.getContractFactory("MyContract");
const myContract = await MyContract.deploy();
await myContract.waitForDeployment();
console.log(`MyContract deployed to ${myContract.target}`);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
6. Smart Contract Security Fundamentals
Adhere to core security principles throughout the development lifecycle.
- Documentation: Use Natspec for all Solidity contracts and functions.
- Minimal On-chain Logic: Pre-process data off-chain to keep on-chain verification simple.
- Upgradeability: Make a deliberate choice (e.g., UUPS proxy, contract migration). Document the procedure rigorously.
- Core Principles: Apply "Defense in Depth," "Least Privilege," and "Secure Defaults."
- Multi-chain Simulation: Test on target L2s (Optimism OP Stack, Base) to validate behavior before mainnet deployment.
7. Avoid Common Hardhat Solidity Test Pitfalls
Hardhat's Solidity testing framework is powerful but has specific conventions.
testFail Anti-pattern: Do not use testFail as a prefix. It's less explicit and not supported by default.
❌ BAD
function testFail_SetGreetingEmpty() public {
myContract.setGreeting(""); // Hardhat won't treat this as a passing test on revert
}
✅ GOOD
function testRevert_SetGreetingEmpty() public {
vm.expectRevert("Greeting cannot be empty");
myContract.setGreeting("");
}
- Foundry Differences: Hardhat does not support:
- Inline configuration via NatSpec.
- Scripting cheatcodes (
startBroadcast, stopBroadcast).
- Foundry's
fixture cheatcodes (rely on programmatic setup in setUp).
getCode and getDeployedCode cheatcodes.
8. Performance and Maintainability
- TypeScript Everywhere: Use TypeScript for all Hardhat scripts, tasks, and tests for static type safety, better IDE support, and fewer runtime errors.
- CI Integration: Integrate your test suite into CI pipelines to automatically vet every pull request.
- Gas Reporting & Coverage: Always enable
hardhat-gas-reporter and solidity-coverage to monitor contract efficiency and test thoroughness.