hardhat — Cursor rule: Hardhat 3 best practices
A Cursor rule that sets Hardhat 3 conventions for your project: TypeScript config, Solidity and TypeScript tests, Ignition deployments.
.cursor/rules/hardhat.mdcWhat hardhat does
The rule loads into Cursor and steers the agent toward Hardhat 3 conventions for Ethereum work. It asks you to move to version 3 right away, since version 2 is deprecated for new development. Configuration lives in hardhat.config.ts with pinned Solidity compiler versions and the optimizer turned on.
Project layout is spelled out folder by folder: contracts and Solidity tests with the .t.sol extension sit in contracts/, while deployment scripts and integration tests are TypeScript files in scripts/ and test/. Mixing JavaScript and TypeScript is called out as wrong. Solidity unit tests use forge-std and vm.expectRevert instead of the testFail prefix; end-to-end scenarios use ethers.js through @nomicfoundation/hardhat-ethers plus chai-matchers.
Deployments go through Hardhat Ignition, where a module declares contracts instead of an imperative script. The config reads several environment variables — SEPOLIA_RPC_URL, PRIVATE_KEY, ETHERSCAN_API_KEY, COINMARKETCAP_API_KEY — so set those up before the related blocks can do anything. One caveat worth knowing: Hardhat's Solidity tests are Foundry-compatible with gaps, no inline NatSpec and no scripting cheatcodes like startBroadcast.
What it helps with
- migrate a project from Hardhat 2 to Hardhat 3
- lay out contracts, tests and scripts without mixing JS and TS
- write Solidity unit tests with forge-std cheatcodes
- declare a deployment as an Ignition module instead of a manual script
- wire gas reporter and solhint into the config
Example prompts for the agent
- Create
hardhat.config.tsfor Hardhat 3 with pinned Solidity versions and the optimizer on - Lay out the project: contracts in
contracts/, TypeScript integration tests intest/ - Declare the deployment as a Hardhat Ignition module in
ignition/modules/MyContract.ts
How to install hardhat
Manually: put the files in place
.cursor/rules/hardhat.mdcFrontmatter: description, globs, alwaysApply
Manually: put the files in place
.claude/rules/hardhat.mdThe paths frontmatter field limits the files: move globs from the source into paths
AGENTS.md
Codex reads AGENTS.md: paste the rule text there
Manually: put the files in place
.github/instructions/hardhat.instructions.mdThe applyTo field sets which files it applies to: move globs from the source into applyTo
Manually: put the files in place
.devin/rules/hardhat.mdManually: put the files in place
.clinerules/hardhat.mdStep by step, with commands for each platform: Cursor rules: .mdc files, four modes and what replaced .cursorrules
Questions about hardhat
How do I install the hardhat rule in Cursor?
Copy the rule file to .cursor/rules/hardhat.mdc in your project root. Cursor picks it up automatically.
What does the hardhat rule do?
It sets Hardhat 3 conventions for the agent: a TypeScript config, folder layout, Solidity and TypeScript tests, and Ignition deployments.
How do Hardhat's Solidity tests differ from Foundry?
They are Foundry-compatible with differences: no inline NatSpec and no scripting cheatcodes like startBroadcast.
Source text
hardhat.mdc · CC0-1.0
---
description: Ensure Hardhat projects follow modern best practices for configuration, testing, deployment, and security using TypeScript and Hardhat 3's Rust runtime.
globs: **/*.{js,jsx}
---
# 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`**
```typescript
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox"; // Includes Ethers, Chai, Verify, etc.
import "@nomicfoundation/hardhat-ignition";
import "hardhat-gas-reporter";
import "solidity-coverage";
import "@nomicfoundation/hardhat-solhint"; // For Solhint
const config: HardhatUserConfig = {
solidity: {
compilers: [
{ version: "0.8.20" }, // Pin specific, stable Solidity versions
{ version: "0.8.21" },
],
settings: {
optimizer: {
enabled: true,
runs: 200,
},
},
},
networks: {
hardhat: {
// Hardhat Network for local development and testing
chainId: 31337,
},
// Example for a testnet:
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",
// Enable Solidity test file convention (e.g., `contracts/MyContract.t.sol`)
// This is the default, but explicitly setting it is good practice.
// tests: { solidity: "./contracts" } // If you want .t.sol files in contracts/
},
gasReporter: {
enabled: process.env.REPORT_GAS !== undefined,
currency: "USD",
coinmarketcap: process.env.COINMARKETCAP_API_KEY,
},
typechain: {
outDir: "typechain-types",
target: "ethers-v6", // Use ethers-v6 for modern projects
},
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`)**
```solidity
// 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`)**
```typescript
import { expect } from "chai";
import { ethers } from "hardhat";
import { MyContract } from "../typechain-types"; // Typechain for type safety
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.setGreeting(newGreeting);
expect(await myContract.greeting()).to.equal(newGreeting);
});
it("should revert if setting an empty greeting", async function () {
await expect(myContract.setGreeting("")).to.be.revertedWith("Greeting cannot be empty");
});
});
```
## 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`)**
```typescript
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**
```typescript
// scripts/deploy.ts (without Ignition)
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**
```solidity
function testFail_SetGreetingEmpty() public {
myContract.setGreeting(""); // Hardhat won't treat this as a passing test on revert
}
```
**✅ GOOD**
```solidity
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.
© sanjeed5 · CC0-1.0 · sanjeed5/awesome-cursor-rules-mdc