Until recently the usual answer to this question was simple. Use Foundry for contracts and tests, keep Hardhat around if the frontend team needs it. Hardhat 3 made that answer less obvious, because it now runs Solidity tests on a Rust runtime too. Both tools are good. They still push a team toward different habits, and that matters more than any benchmark.
The Short Version
| Area | Hardhat | Foundry |
|---|---|---|
| Main test language | TypeScript with viem or ethers, plus Solidity tests in Hardhat 3 | Solidity with forge-std |
| Speed | Much faster in Hardhat 3 thanks to the Rust-based EDR runtime | Very fast compile and test loop, still the reference point |
| Fuzz and invariant tests | Supported in Hardhat 3 Solidity tests, younger tooling | Mature, with stateful invariant testing and good shrinking |
| Mainnet forking | Fork config per network, works in tests and scripts | forge test --fork-url or vm.createSelectFork inside tests |
| Deployment | Hardhat Ignition modules with resumable deployments | forge script with vm.startBroadcast and --broadcast |
| Extra tools | Large plugin ecosystem, tight TypeScript integration | cast for chain calls, anvil local node, chisel REPL |
| Dependencies | npm packages | Git submodules or Soldeer, npm also works with remappings |
Tests: TypeScript or Solidity
This is the real difference between the two. In a classic Hardhat project, tests are written in TypeScript. You deploy contracts, call them through viem or ethers, and assert on the results with Mocha or the Node test runner. It feels natural to web developers, and it tests the contract the same way your dApp frontend will call it. The downside is noise. Every call is async, every number is a bigint, and a simple revert check takes several lines.
Foundry tests are Solidity contracts. A function named testTransfer is a test. Cheatcodes such as vm.prank, vm.warp, vm.deal, and vm.expectRevert let you change the caller, move time forward, fund accounts, and check errors in one line each. Developers who write Solidity all day tend to prefer this, because they never switch languages. Auditors like it too, since most proof-of-concept exploits are now published as Foundry tests.
Hardhat 3 supports Solidity tests that follow the same forge-std style, so you can write unit tests in Solidity and keep TypeScript for integration tests that need offchain logic. That is a good split, and it is the main reason the gap between the tools has narrowed.
Speed
Foundry is written in Rust and has always been fast. On a token project with a few hundred tests, forge test typically finishes in seconds after the first compile, while a similar suite in Hardhat 2 with Mocha could take a minute or more. Hardhat 3 moved its execution to EDR, also written in Rust, and its Solidity tests run in a similar range to Foundry. TypeScript tests are still slower because of the JavaScript side, but for most teams the difference is no longer a reason to switch by itself.
Fuzzing and Invariant Testing
Give a Foundry test function parameters and it becomes a fuzz test. Forge calls it hundreds of times with random inputs and shrinks any failure to the smallest example. Invariant tests go further. You define properties that must always hold, such as the sum of all balances equals totalSupply, and Foundry calls random sequences of functions on a handler contract to try to break them. This kind of test catches bugs that hand-written cases miss, especially in lending and AMM math.
Hardhat 3 Solidity tests support fuzzing as well. In our experience Foundry still has the more mature setup for invariant testing, more examples online, and better integration with tools such as Echidna and Medusa through shared test harnesses. If fuzzing is central to your security process, Foundry is the safer bet today.
Mainnet Forking
Both tools can fork a live network at a pinned block so you can test against real Uniswap pools, real Chainlink feeds, and real token balances. In Hardhat you add a forking URL and block number to the network config. In Foundry you pass --fork-url on the command line or call vm.createSelectFork inside a test, which also makes it easy to test across two chains in one file. Always pin the block number. Otherwise tests start failing on their own when state changes, and the RPC provider has to serve fresh data on every run.
Deployment Scripts
Hardhat Ignition describes deployments as modules. You declare which contracts to deploy and which calls to make, and Ignition works out the order, sends the transactions, and records progress in a journal. If a deployment fails halfway, rerunning it continues from where it stopped. That is valuable for systems with ten contracts and a dozen setup calls.
Foundry uses forge script. A deployment is a Solidity script where everything between vm.startBroadcast and vm.stopBroadcast becomes a real transaction. It runs as a simulation first, then sends with --broadcast and verifies on Etherscan with --verify. It is simple and very readable. Resuming a partial deployment exists with --resume, but you need to think about it more yourself than with Ignition.
Plugins and Ecosystem
- Hardhat has a long list of plugins, including contract verification, the OpenZeppelin Upgrades plugin, and integrations with TypeChain-style typing through viem. Hardhat 3 changed the plugin system, so check that the plugins you rely on support version 3 before upgrading.
- Foundry ships most tools in the box. cast reads and writes chain data from the terminal, anvil runs a local node, and chisel lets you try Solidity snippets interactively. OpenZeppelin also maintains a Foundry Upgrades library.
- Frontend teams usually prefer Hardhat because contract types and ABIs flow straight into a TypeScript dApp.
- Security researchers and audit firms mostly work in Foundry, which makes it easier to share findings as runnable tests.
Using Both in One Repo
You do not have to choose only one. A setup we use often keeps contracts in one folder, runs unit, fuzz, and invariant tests with forge, and uses Hardhat for TypeScript integration tests and Ignition deployments that the backend team maintains. The two tools need to agree on import paths, so keep a single remappings.txt and pin the same compiler version and optimizer settings in both configs. Otherwise you will test one bytecode and deploy another.
How to Choose
- 1Look at who writes the tests. A team of Solidity specialists will move faster in Foundry. A full-stack team that mostly writes TypeScript will be more comfortable in Hardhat.
- 2Check how much the contracts hold. Protocols that manage real value need fuzz and invariant testing, which points to Foundry or at least Foundry-style tests.
- 3Count the moving parts in deployment. Many contracts with complex setup favor Ignition. A few contracts with simple setup work fine with forge script.
- 4Check the plugins and libraries you already depend on, and whether they support Hardhat 3.
- 5Pick one as the default for new work, document it in the README, and do not let each developer choose their own.
The best tool is the one your whole team will actually write tests in. A fast test runner with no tests protects nothing.
In our smart contract and Ethereum training we teach both tools side by side, starting with Foundry for unit and fuzz tests and showing how Hardhat fits in when a TypeScript dApp and deployment pipeline enter the picture.
Key takeaways
- Foundry tests are written in Solidity with cheatcodes. Hardhat tests are mainly TypeScript, and Hardhat 3 adds Solidity tests too.
- Hardhat 3 closed most of the speed gap, so speed alone is no longer a strong reason to switch.
- Foundry has the more mature fuzz and invariant testing, which matters for contracts that hold real value.
- Ignition handles complex, resumable deployments well. forge script is simpler and fits smaller deployments.
- Running both in one repo works if they share remappings, compiler version, and optimizer settings.


