ByAUJay
Summary: DeFi teams ship faster and safer when they split testing by intent: Foundry for high‑fidelity Solidity unit/fuzz/invariant testing and Hardhat for TypeScript/viem end‑to‑end and procurement‑friendly workflows. Below is a pragmatic blueprint—what breaks in real forks, where CI time disappears, and how to harden your stack with gas snapshots, invariant metrics, and supply‑chain guardrails.
Testing Strategies: Foundry vs. Hardhat for DeFi Protocols
Audience: DeFi protocol teams (keywords: Gas optimization, mainnet forking, invariant testing, MEV simulation, audit readiness, RPC costs)
—
Your forked tests are flaky, gas regressions slip into prod, and audits keep finding “should’ve been caught” invariants
If you’re maintaining a composable protocol (e.g., ERC-4626 vaults routing through Uniswap v3/v4, on L2, with keepers and oracle adapters), your test suite probably shows these symptoms:
- Fork tests intermittently fail due to upstream state drift, slow archive calls, and inconsistent impersonation across clients. Hardhat forking needs an archive endpoint and a pinned block for stability; failure to pin explodes CI noise and excuses “it passed locally.” (hardhat.org)
- Coverage takes too long, so teams stop running it on every PR. Then a week before audit, you turn on coverage and find “Stack too deep” or missing branches. Foundry v1.0 significantly accelerates coverage and fuzzing, but many teams haven’t upgraded and still rely on slow or plugin‑chained flows. (paradigm.xyz)
- Invariant tests exist, but they don’t surface minimal failing traces, so repro takes hours—especially for liquidity pool invariants that fail only after dozens of steps. Foundry v1.0 rewrote shrinking and added invariant metrics; if you haven’t adopted, you’re paying an invisible tax. (paradigm.xyz)
- Gas regressions sneak in. You rely on ad‑hoc reports or plugins; engineers avoid running heavy test tasks locally. Native gas snapshotting and diff gates exist now; you may not be using them. (learnblockchain.cn)
- Procurement/security risk: plugin sprawl in Node projects, unpinned viem type changes, and recent supply‑chain attacks against “extended” Hardhat toolboxes increase risk around keys and CI runners—exactly when you scale contributors. (v2.hardhat.org)
Each of these adds real business risk and cost
- Missed deadlines: Slow coverage/fuzz cycles block merges; a 40% time penalty across a squad is measured in weeks, not hours. Foundry v1.0 benchmarks show faster compilation and faster fuzz/coverage; not upgrading leaves ROI on the floor. (paradigm.xyz)
- Launch risk: Flaky fork tests mask integration bugs with external AMMs, oracles, and L2 bridges. Hardhat’s forking + impersonation is powerful, but without pinned blocks, custom headers, and consistent helpers, the test signal is unreliable. (hardhat.org)
- Audit cost: Without property‑based invariants for core invariants (e.g., “totalAssets never decreases without withdrawal,” “oracle‑quoted price monotonic within bounds”), auditors spend paid time reproducing basics.
- Supplier risk: 2025’s npm supply‑chain incident hit a viem‑toolbox variant; if your CI installs the world on each run, you multiply blast radius. Locking official Hardhat toolboxes and pinning viem types is no longer optional. (advisories.gitlab.com)
- ZK/L2 edge cases: zkEVM test harnesses have cheatcode limitations at call depth; if you only test on L1 semantics you’ll learn too late that your “harmless” prank doesn’t work under zk constraints. (foundry-book.zksync.io)
—
A dual‑track testing architecture implemented with 7Block’s methodology
We deploy a crisp division of labor: Foundry for Solidity‑native correctness, fuzzing, invariants, and gas regressions; Hardhat for TypeScript/viem end‑to‑end scenarios, deployment recipes, and enterprise‑friendly procurement.
1) Foundry for Solidity‑native speed, invariants, and gas guarantees
What we implement:
- Foundry v1.0 baseline with invariant metrics and replayable failures (fast shrinking, decoded internals, flamegraphs), enabling you to fix bugs not debug the framework. Benchmarks show 2x improvements vs v0.2, faster coverage, and better invariant shrinking. (paradigm.xyz)
- Deterministic mainnet forks in tests (cheatcodes create/select/roll multiple forks), pinning block numbers and rolling strategically to touch oracle windows or liquidity shifts—without leaking state between forks. (learnblockchain.cn)
- Gas regression gating using forge’s native snapshot file with CI “check/diff” to block merges on regressions > tolerance; no bespoke plugins needed. (learnblockchain.cn)
- Compiler pipeline tuned for “gas per op” targets: we evaluate via‑IR per module for net gas improvements; with Solidity’s 2024 guidance, via‑IR is on a path to default and often yields better gas while reducing stack‑too‑deep churn. We lock profiles for coverage vs deploy builds to avoid skew. (soliditylang.org)
- Evolving EVM testing: enable Prague/Pectra features (e.g., EIP‑7702) early to validate account‑abstraction interactions before L1 enables them, avoiding late surprises in keeper or sponsor flows. (paradigm.xyz)
Practical Foundry example: stateful invariant with a handler (ERC‑4626 vault safety)
// forge-std 1.x pragma solidity ^0.8.21; import {Test, console2} from "forge-std/Test.sol"; import {StdInvariant} from "forge-std/StdInvariant.sol"; import {IERC20} from "openzeppelin-contracts/token/ERC20/IERC20.sol"; import {IERC4626} from "openzeppelin-contracts/interfaces/IERC4626.sol"; contract VaultHandler { IERC4626 public vault; IERC20 public asset; constructor(IERC4626 _vault) { vault = _vault; asset = IERC20(_vault.asset()); } function deposit(uint256 amt) external { amt = bound(amt, 1e6, 1e24); asset.approve(address(vault), amt); vault.deposit(amt, address(this)); } function redeem(uint256 shares) external { shares = bound(shares, 1, vault.balanceOf(address(this))); vault.redeem(shares, address(this), address(this)); } } contract Invariants_Vault is StdInvariant, Test { IERC4626 vault; VaultHandler handler; function setUp() public { // Fork mainnet at a pinned block for deterministic behavior uint256 fork = vm.createFork(vm.envString("MAINNET_RPC_URL"), 21_000_000); vm.selectFork(fork); // Assume vault already deployed at address VAULT on mainnet vault = IERC4626(vm.envAddress("VAULT")); handler = new VaultHandler(vault); // Target the handler for stateful fuzzing bytes4[] memory sels = new bytes4[](2); sels[0] = VaultHandler.deposit.selector; sels[1] = VaultHandler.redeem.selector; targetSelector(FuzzSelector({addr: address(handler), selectors: sels})); targetContract(address(handler)); } // Money invariants function invariant_Solvency() external { // Total assets must be >= total supply scaled by conversion (ceil) assertGe(vault.totalAssets(), vault.convertToAssets(vault.totalSupply())); } function invariant_NoPhantomShares() external { // Price per share never negative and monotonic across fuzz sequences assertGe(vault.convertToShares(1e18), 0); } }
This harness uses Foundry’s invariant runner, handler targeting, and fork pinning to validate real deployed vault math under stateful fuzzing. Shrinking produces minimal failing sequences when invariants break. (learnblockchain.cn)
Gas regression gate (CI):
# Generate or update baseline forge snapshot --snap .gas-snapshot # Block merges if gas changed beyond 2% forge snapshot --check --tolerance 2
Foundry writes deterministic gas snapshots that you can diff in CI—actionable “money phrase”: catch gas regressions before they hit TVL. (learnblockchain.cn)
2) Hardhat for realistic E2E flows, deployment orchestration, and procurement hygiene
What we implement:
- Hardhat 3 with both Solidity tests and TypeScript (node:test) harnesses. Solidity tests bring Foundry‑style speed and fuzzing parity inside Hardhat; TypeScript tests model real user flows (multi‑tx, events, time). Built‑in coverage in Hardhat 3 reduces plugin maintenance. (hardhat.org)
- Viem‑based toolbox for thin, typed clients, while pinning viem to avoid type‑churn and breakages (Nomic Foundation’s docs recommend pinning). (v2.hardhat.org)
- Deterministic mainnet forking (block pin, custom headers, impersonation) with helpers to set balances, time, and gas; all under a single command in CI. (hardhat.org)
- Deployment recipes via Ignition modules (or your existing deploy system) consolidated and versioned; note the Ignition package moved and its repo lifecycle—keep versions managed inside the Hardhat 3 toolboxes. (hardhat.org)
Practical Hardhat example: end‑to‑end swap on a fork with viem and impersonation
// node:test + viem, Hardhat 3 import { test } from "node:test"; import assert from "node:assert/strict"; import { network } from "hardhat"; // Example: quote & execute a swap on a forked UniswapV3 pool with an impersonated whale test("forked swap end-to-end", async () => { const { viem } = await network.connect(); // Fork config comes from hardhat.config.ts const publicClient = await viem.getPublicClient(); const walletClient = await viem.getWalletClient(); // Pin to a block in config or via CLI: --fork-block-number 20500000 const block = await publicClient.getBlockNumber(); // Impersonate a token whale & set balance const helpers = await import("@nomicfoundation/hardhat-network-helpers"); await helpers.impersonateAccount("0xWhale..."); const whale = await viem.impersonateAccount("0xWhale..."); await helpers.setBalance("0xWhale...", 10n ** 20n); // 100 ETH // Approve & swap (pseudo ABI) const tokenIn = await viem.getContractAt("IERC20", "0xTokenIn"); const router = await viem.getContractAt("ISwapRouter", "0xRouter"); await tokenIn.write.approve([router.address, 10n ** 24n], { account: whale.account }); const before = await tokenIn.read.balanceOf([whale.account.address]); // Execute swap (exactInputSingle) await router.write.exactInputSingle([{ tokenIn: tokenIn.address, tokenOut: "0xTokenOut", fee: 3000, recipient: whale.account.address, deadline: BigInt(Date.now()/1000 + 600), amountIn: 1_000_000_000_000_000_000n, amountOutMinimum: 0n, sqrtPriceLimitX96: 0n }], { account: whale.account }); const after = await tokenIn.read.balanceOf([whale.account.address]); assert.equal(after < before, true); });
Hardhat’s forking, impersonation, and helpers make cross‑protocol flows reproducible, while viem keeps the client thin and typed. Pin the fork block to eliminate drift and CI flakiness. (hardhat.org)
Built‑in Solidity test with coverage (Hardhat 3):
# Run only Solidity tests with coverage in HH3 npx hardhat test --coverage --group solidity
Hardhat 3 adds native coverage; no more juggling coverage plugins. Money phrase: fewer moving parts, fewer supply‑chain nodes. (hardhat.org)
3) ZK/L2‑aware testing
- For zkEVM (e.g., zkSync), some cheatcodes must execute at the root of the test context, not inside nested calls. We isolate those cases, and mirror oddities (timestamp, gas metering, precompiles) in a dedicated profile so your invariants stay truthful across L1 and ZK. (foundry-book.zksync.io)
- For OP‑stack or chains with different gas/refund rules, Hardhat 3’s multichain simulation improves alignment of TypeScript tests with on‑chain reality. (hardhat.org)
—
When to use which (short version)
- Use Foundry when the job is correctness, speed, and gas: fuzzing, invariants, “why did this revert?”, gas diffs, and running 10k test cases fast. Foundry v1.0 introduced faster coverage and invariant shrinking, decoded internals, replay of failures, and gas snapshot cheatcodes. (paradigm.xyz)
- Use Hardhat when the job is orchestration and E2E: real user‑like transaction sequences, deployment plans, explorer verification, and procurement‑friendly Node/TypeScript. Hardhat 3 adds Solidity tests, viem toolbox, and built‑in coverage. (hardhat.org)
We combine both: Foundry for inner loops (CI on every PR) and Hardhat for outer loops (nightly E2E on forked networks + deployment rehearsals).
—
Implementation blueprint (what we set up for you)
- Profiles and build matrix
- Foundry profiles for test, coverage, and deploy; via‑IR selectively enabled per package with branch‑specific gates. Solidity 0.8.28 adds IR and transient storage improvements—useful for advanced accounting patterns. (soliditylang.org)
- Hardhat 3 config with declarative build profiles and typed artifacts across TS tests. (hardhat.org)
- Deterministic forks and helpers
- Foundry:
,createSelectFork
, and explicitrollFork
to make invariants reproducible; multiple chain forks in one file for cross‑chain adapters. (learnblockchain.cn)vm.deal/prank/warp - Hardhat: mainnet forking with pinned block and archive RPC;
, balance manipulation, and custom headers for auth. (hardhat.org)getImpersonatedSigner
- Gas optimization guardrails
and “gas budget” thresholds per module. Money phrase: automatic gas gates prevent silent unit cost creep. (learnblockchain.cn)forge snapshot --check --tolerance N
- Invariant library and metrics
- Canonical invariants for vaults/AMMs/liquidations including solvency, monotonicity, and fee accounting. Foundry v1.0’s invariant metrics give call counts and discard rates to prove coverage depth. (paradigm.xyz)
- Coverage everywhere
- Foundry:
+ VSCode gutters and LCOV HTML so reviewers see uncovered lines in PRs. (rareskills.io)forge coverage --report lcov - Hardhat 3:
without external plugins simplifies CI. (hardhat.org)--coverage
- Procurement and supply‑chain hygiene
- Lock official toolboxes only; avoid “extended” variants that were compromised in 2025; rotate keys if any suspicious deps were installed. Pin viem due to type churn. Money phrase: reduce blast radius in CI. (advisories.gitlab.com)
- ZK profiles
- Dedicated zkSync Foundry profile with root‑level cheatcode discipline (documented limitations) and mirrored test semantics so results match zkEVM. (foundry-book.zksync.io)
—
Proof: GTM‑relevant metrics you can expect
- Faster CI feedback loops: Foundry v1.0 shows up to 40% faster fuzz/coverage execution and 3.5x faster coverage in benchmarks—translating directly into fewer blocked PRs and reduced CI minutes. (paradigm.xyz)
- Lower integration risk: Hardhat’s built‑in coverage + viem toolbox and fork pinning removes plugin mismatch and semver surprises; viem pinning is recommended by Nomic Foundation. (hardhat.org)
- Gas cost discipline: native snapshots and diff‑gated PRs stop regressions before they shift unit economics for users. (learnblockchain.cn)
- Flake reduction: fork pinning plus impersonation helpers materially reduce test nondeterminism; HH docs prescribe pinned block numbers and archive RPCs for reliability. (hardhat.org)
- Security posture: reduction in third‑party plugin surface area (built‑in coverage, official toolboxes) combined with allow‑listed deps avoids incidents like the 2025 npm compromise. (advisories.gitlab.com)
—
Practical patterns you can copy‑paste today
- Foundry multi‑fork test to compare AMM quotes cross‑chain
pragma solidity ^0.8.20; import {Test} from "forge-std/Test.sol"; contract QuotesDiffTest is Test { uint256 mainnet; uint256 optimism; function setUp() public { mainnet = vm.createFork(vm.envString("MAINNET_RPC_URL"), 20_900_000); optimism = vm.createFork(vm.envString("OPTIMISM_RPC_URL"), 121_000_000); } function testDiff_UniswapV3Quote() public { vm.selectFork(mainnet); uint256 q1 = _quoteUniswapV3(1e18); vm.selectFork(optimism); uint256 q2 = _quoteUniswapV3(1e18); // Assert spread within tolerance assertLt(_absDiff(q1, q2) * 10_000 / ((q1+q2)/2), 50); // <0.5% } function _quoteUniswapV3(uint256 amountIn) internal view returns (uint256) { // call into Quoter on the active fork... return amountIn; // replace with real call } }
Switching forks within a single test isolates state while preserving test contract variables—ideal for cross‑chain adapters. (learnblockchain.cn)
- Hardhat Solidity tests alongside TS tests (HH3)
- Put
files next to contracts for unit‑level solidity tests..t.sol - Use
for TypeScript flows andnpx hardhat test nodejs
for everything; addnpx hardhat test
when needed. (hardhat.org)--coverage
- Coverage visualization for reviewers (Foundry)
forge coverage --report lcov genhtml lcov.info --branch-coverage --output-dir coverage # open coverage/index.html
Turn coverage into a PR artifact reviewers actually use. (rareskills.io)
- Handling flaky forks (Anvil/HH Network)
- Use
and pin block numbers; cache RPC responses for repeated runs; choose high‑quality archive RPCs to avoid timeouts under heavy traces. (getfoundry.sh)--fork-url
—
Tooling notes that save hours (and RPC credits)
- Foundry logging/traces: use
+ andforge test -vvv
to see internal calls and hotspots; replay failing tests deterministically. (paradigm.xyz)--flamegraph - Anvil modes: interval or on‑demand mining improves realism for keeper flows; preinstall CREATE2 deployer for reproducible addresses. (getfoundry.sh)
- Solidity compiler pipeline: track via‑IR improvements; since 0.8.26/0.8.28, IR optimizations and features (e.g., custom errors in require, transient storage) make IR pipelines more attractive for production builds; profile, don’t guess. (soliditylang.org)
—
How 7Block Labs executes (and why it pays back quickly)
- We set up dual‑track CI: Foundry fast lane per PR with gas/invariant gates, and a nightly Hardhat E2E suite across forked mainnet and your target L2s—so you get speed where it matters and realism where it counts.
- We standardize deploy paths via Hardhat Ignition modules or your preferred deployment stack, and wire verification, tagging, and rollback points.
- We write invariants that map to business KPIs: solvency, fee capture, and slippage bounds—so “green tests” correlate with TVL safety, not just bytes saved.
- We align with your auditors: invariants and property tests written in auditor‑friendly patterns, plus coverage reports consumable in their workflows.
Back this with follow‑on services if you need them:
- End‑to‑end protocol builds via our DeFi development services and smart contract development.
- Security hardening via security audit services and gas‑focused reviews.
- Composability and integrations through dApp development and cross‑chain solutions.
- If you’re still scoping the build, our blockchain development services and web3 development services cover the end‑to‑end lifecycle.
—
Bottom line
- Use Foundry where it shines: invariants, fuzzing, gas snapshots, decoded traces, and rapid iteration—now faster in v1.0. (paradigm.xyz)
- Use Hardhat where it shines: TypeScript/viem E2E, deterministic forks, and built‑in coverage with a procurement‑friendly toolbox. Pin and audit dependencies. (hardhat.org)
- Glue them with discipline: pinned blocks, profiles, CI gates, and supply‑chain hygiene.
7Block Labs can implement this in days—not quarters—and tie it to measurable ROI: fewer flaky builds, faster merges, earlier invariant catches, and enforced gas budgets.
Ready to de‑risk your launch and speed up your roadmap?
Book a DeFi Testing Strategy Call.
Like what you're reading? Let's build together.
Get a free 30‑minute consultation with our engineering team.

