Foundry vs Hardhat in 2026: Which Toolchain for Contract Audits
Foundry vs Hardhat for audit-grade testing: why fuzzing-first Foundry workflows surface bugs static Hardhat suites miss, and when Hardhat still wins.
{"excerpt":"Foundry vs Hardhat for audit-grade testing: why fuzzing-first Foundry workflows surface bugs static Hardhat suites miss, and when Hardhat still wins.","tags":["Smart Contracts","Foundry","Hardhat","Security Audits","Solidity Testing"],"cover":"terminal","content":"## The bug that unit tests never would have found\n\nA client came to us with a lending contract that had 94% branch coverage in Hardhat and a green CI badge on every PR. We ported the core invariants to Foundry, wrote three fuzz tests targeting the collateral ratio math, and within about 40,000 runs forge test found a rounding case where a borrower could withdraw collateral against a liquidated position if the oracle price update landed in the same block as the withdrawal call. Nobody wrote that test case by hand. The fuzzer found it because it doesn't know what a "normal" input looks like — it just mutates bytes until something reverts or an invariant breaks.\n\nThat's the real difference between Foundry and Hardhat in 2026, and it's not really about syntax or plugin ecosystems. It's about what kind of bugs each toolchain is structurally biased toward finding.\n\n## Foundry's default posture is adversarial\n\nForge tests are Solidity, compiled with the same compiler and optimizer settings as production code, running in a native EVM implemented in Rust (revm). Every test_ function that takes arguments is a fuzz test by default — no extra setup, no separate fuzzing library to wire in. Write this:\n\nsolidity\nfunction testFuzz_WithdrawNeverExceedsCollateral(uint96 amount, uint32 priceDelta) public {\n vm.assume(amount > 0 && amount < 1e30);\n _seedPosition(amount);\n oracle.setPrice(oracle.price() + priceDelta);\n uint256 before = collateral.balanceOf(user);\n vault.withdraw(amount);\n assertLe(collateral.balanceOf(user) - before, _maxSafeWithdraw());\n}\n\n\nand forge test runs it hundreds of times with edge-case-biased random inputs — zero, max uint, boundary values near your require statements — for free. Add forge test --invariant and you get stateful fuzzing: a fuzzer that calls a random sequence of your public functions in random order and checks that system-wide invariants (total supply conservation, solvency, no negative balances) hold after every call. This is the closest thing to what an actual attacker does: chain unexpected calls together and see what breaks.\n\nHardhat has none of this natively. You bolt on fuzzing via hardhat-foundry (which just runs Forge underneath) or a JS-based property library, but it's not the default mental model of anyone writing a Hardhat test suite. Most Hardhat suites we review are example-based: deposit 100, withdraw 100, assert balance is 0. That style catches regressions. It does not catch the input you didn't think of.\n\n## Where Hardhat still wins\n\nHardhat's strength is the JavaScript/TypeScript ecosystem and how naturally it plugs into everything around the contract — the frontend, the subgraph, the deployment scripts, the multisig tooling. If your team already has TypeScript types generated from ABIs via viem or typechain, and your deploy pipeline is Hardhat Ignition modules feeding into the same repo as your Next.js app, ripping that out to chase fuzzing coverage is usually the wrong call. Hardhat Network's console.log inside Solidity, its mainnet-forking impersonateAccount, and its stack-trace decoding on revert are still genuinely better developer experience for someone debugging a specific failing transaction rather than searching for unknown ones. When we're doing smart contract development for a client who also wants us building the dApp frontend against the same contracts, Hardhat's TS-native tooling saves real integration time versus gluing Foundry's Solidity-only world to a JS frontend team.\n\nCompile times matter too, in the other direction. Forge's Rust-based compiler pipeline and caching are faster than Hardhat's for large repos once you're past a few hundred contracts — we've seen 3-4x differences on monorepos with heavy OpenZeppelin and Uniswap-v4-hook-style dependency trees.\n\n## The comparison, dimension by dimension\n\n| Dimension | Foundry | Hardhat |\n|---|---|---|\n| Fuzz testing | Native, zero-config, every parameterized test | Bolted on, rarely used by default |\n| Invariant/stateful fuzzing | Built in (forge test --invariant) | Not native; needs Echidna/Foundry integration |\n| Test language | Solidity | JavaScript/TypeScript |\n| Debugging failed tx | forge debug, stack traces, cast decode | console.log, Hardhat Network stack traces |\n| Mainnet forking | Fast, built-in --fork-url | Built-in, mature impersonateAccount flow |\n| Compile/test speed (large repos) | Faster, Rust toolchain | Slower on big dependency trees |\n| Ecosystem/plugins | Growing but Solidity-centric | Deep JS ecosystem, typechain, ignition |
| Gas snapshotting | forge snapshot, diffable in CI | hardhat-gas-reporter, table output |\n| Symbolic/differential testing | Halmos, Foundry ffi for reference impls | Possible but more manual wiring |\n| Learning curve for JS teams | Steeper — need real Solidity test-writing skill | Gentle — same language as the app |\n\n## What we actually run in audits\n\nOn every smart contract audit we run, Foundry is the primary harness regardless of what the client built with, because invariant fuzzing is how we generate the attack sequences a manual reviewer would take days to enumerate by hand. If the client shipped Hardhat, we don't rewrite their suite — we add a parallel test/invariant/ tree in Forge, point it at the same deployed bytecode via --fork-url against a local fork, and let it run overnight in CI with a high runs count (we typically set fuzz.runs = 10000 for audit-grade passes, versus the default 256 for day-to-day dev). That overnight run has caught reentrancy paths and oracle-timing bugs that four days of manual code review missed on the first pass, then confirmed once we knew where to look.\n\nThe pattern generalizes past EVM contract logic, too — the same instinct of "assume the adversary controls timing and ordering, not just inputs" is why we push clients toward MEV-aware architectures, the kind of ordering risk we cover in our breakdown of bundle-based versus public-mempool MEV protection, and why resolution-timing edge cases matter so much in oracle design, which we dug into separately when comparing UMA's optimistic oracle against Chainlink for market resolution. Different chains, same underlying question: what happens when execution order isn't what you assumed at design time.\n\n## The verdict\n\nIf you're an audit shop, a protocol team pre-mainnet, or anyone whose job is finding the input nobody thought of, Foundry should be your primary test harness — full stop. The invariant fuzzer alone justifies the switch, and the Solidity-native test language means your test suite is written by people who actually understand the EVM semantics they're testing, not people translating intent through a JS abstraction layer.\n\nIf you're a product team shipping a dApp where the contracts are a means to a UI and your velocity depends on shared TypeScript types across frontend, scripts, and tests, keep Hardhat as your development and deployment harness — it's still the better DX for that workflow — but bring Foundry in as a second, adversarial test suite before any audit or mainnet deploy. Running both isn't overhead; it's the same reason we look at architectural divergence across chains, like the split between HyperCore and HyperEVM execution environments, before deciding where risk actually lives. Pick the tool that matches the question you're asking, and for "can this contract be broken," fuzzing wins every time.\n\nIf you want that adversarial pass run against your contracts before mainnet rather than after an incident, that's what our audit team does for a living."}
Need a bot like this built?
We design, build and run trading bots on Solana, Hyperliquid and Polymarket.
Start a projectMore from the blog
Public vs Paid RPC: How to Run a Fair Benchmark
A fair comparison of public and paid RPC endpoints must account for quotas, workload, freshness and support guarantees.
Read articleRPC Benchmark Percentiles Explained: p50, p95 and p99
Why averages hide the latency spikes that break trading bots, wallets and production blockchain applications.
Read articleHyperliquid REST vs WebSocket Benchmark: What to Measure
A benchmark plan for Hyperliquid market data that separates request latency from streaming freshness and recovery.
Read article