Both are mature, actively maintained, and used for serious production contracts. The real difference is what language your tests and scripts are written in, and that follows from who is on your team and what else your project contains.
Short answer: if your team is mostly Solidity developers and the contracts are the product, start with Foundry. If the contracts sit inside a TypeScript codebase (a dApp frontend, a backend, deployment pipelines in Node), Hardhat fits more naturally. Plenty of teams use both.
What each one is
Foundry is a set of command-line tools written in Rust:
forge: build, test, fuzz, measure coverage and gas, and run deployment scripts;cast: talk to any chain from the shell (send transactions, call functions, decode data);anvil: a local node, with optional mainnet forking;chisel: a Solidity REPL.
Tests and deployment scripts are Solidity contracts. Dependencies are usually git repositories installed with forge install.
Hardhat is a Node.js framework from the Nomic Foundation. It includes a local development network, a task runner and a large plugin ecosystem (contract verification, deployments with Hardhat Ignition, coverage, gas reporting). Tests and scripts are traditionally written in TypeScript or JavaScript with ethers or viem, and dependencies come from npm.
The same test in both
Foundry, in Solidity (against the Counter contract that forge init generates):
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test, stdError} from "forge-std/Test.sol";
import {Counter} from "../src/Counter.sol";
contract CounterTest is Test {
Counter counter;
function setUp() public {
counter = new Counter();
}
// Any function with parameters is a fuzz test: forge runs it with many random inputs.
function testFuzz_SetNumber(uint256 x) public {
counter.setNumber(x);
assertEq(counter.number(), x);
}
function test_IncrementOverflowReverts() public {
counter.setNumber(type(uint256).max);
vm.expectRevert(stdError.arithmeticError); // cheatcode: next call must revert
counter.increment();
}
}
Hardhat, in TypeScript with the ethers toolbox (Hardhat 2 style):
import { expect } from "chai";
import { ethers } from "hardhat";
describe("Counter", () => {
it("sets the number", async () => {
const counter = await ethers.deployContract("Counter");
await counter.setNumber(42n);
expect(await counter.number()).to.equal(42n);
});
});
Hardhat 3 changed how you get a connection: you import network from "hardhat" and call await network.connect() to get ethers or viem. It can also run Solidity tests written in the same style as Foundry's, so the gap between the two tools has narrowed. Check which major version a project uses before copying examples.
Where Foundry is stronger
- Speed. Compilation and test execution are fast, and the feedback loop matters when you run the suite hundreds of times a day.
- Fuzzing and invariant testing are built in. Add a parameter and you have a property test. Invariant tests call random sequences of functions and check that properties (e.g. "total supply equals the sum of balances") always hold. This finds bugs that example-based tests miss.
- Cheatcodes.
vm.prank,vm.deal,vm.warp,vm.expectRevert,vm.expectEmitand many more make it easy to set up exact states. - No context switch. You test contracts in the language they're written in, with the same types. There are no
BigIntconversions or ABI encoding surprises. caston its own is worth installing, even if you use Hardhat for everything else.
Where Hardhat is stronger
- JavaScript/TypeScript integration. Deployment scripts, frontends, bots and backend services can share code, types and ABIs. Integration tests that look like your real client code are simpler to write.
- Plugin ecosystem. Verification, upgrades (
@openzeppelin/hardhat-upgrades), Ignition deployments, and many third-party plugins hook into one config file. - Familiar debugging.
console.logfromhardhat/console.soland readable Solidity stack traces on the Hardhat network. (Foundry supportsconsole.logtoo, and-vvvvgives full call traces.) - Onboarding. Web developers who already know Mocha, Chai and npm are productive on day one.
Using both
A common setup is Foundry for unit, fuzz and invariant tests and Hardhat for TypeScript deployment scripts and integration with the rest of the stack. To make it work:
- keep one source directory and one set of compiler settings (version, optimizer runs,
via_ir) in both configs, otherwise the two tools produce different bytecode; - make sure imports resolve the same way in both (Foundry remappings versus npm package paths);
- decide which tool produces the artifacts you deploy and verify.
How to decide
| If... | Pick |
|---|---|
| The team writes Solidity all day and wants fuzzing and invariants | Foundry |
| Contracts ship alongside a TypeScript app or backend | Hardhat |
| You depend on specific Hardhat plugins in your deploy pipeline | Hardhat |
| You need fast, deep testing of DeFi logic against a mainnet fork | Foundry (Hardhat can fork too) |
| You're learning Solidity and already know JS | Either; Foundry teaches you more Solidity |
Switching later is not a rewrite of your contracts, only of your tests and scripts, so don't agonize over it.
Checklist
- Pick the tool whose test language matches the people writing the tests.
- Whatever you choose, use fuzz and invariant tests for anything that holds funds.
- Install
castregardless; it's the best general-purpose chain CLI. - If you run both, share compiler settings and import resolution.
- Pin tool versions in CI so a toolchain update doesn't change your bytecode unnoticed.
Get the weekly commit
New blockchain deep dives every week.