ByAUJay
Testing Strategies: Foundry vs. Hardhat for DeFi Protocols
Your forked tests are flaky, gas regressions slip into prod, and audits keep finding “should’ve been caught” invariants
If you're working on a composable protocol--like ERC-4626 vaults that route through Uniswap v3 or v4 on Layer 2, complete with keepers and oracle adapters--there’s a good chance your test suite is displaying some of these signs:
- Fork tests can be pretty unreliable. They often trip up because of upstream state drift, sluggish archive calls, and inconsistent impersonation among clients. If you're forking with Hardhat, you really need an archive endpoint and a pinned block for some solid stability. If you don’t pin, you end up with a lot of CI noise and those frustrating excuses like “it passed locally.” Check out more at (hardhat.org).
- Running coverage often takes forever, which leads teams to skip it on every PR. Then, right before an audit, you switch it on and are hit with issues like “Stack too deep” or missing branches. Good news? Foundry v1.0 speeds up coverage and fuzzing dramatically, but a lot of teams haven’t made the jump yet and are still dealing with slower, plugin-heavy processes. More info can be found at (paradigm.xyz).
- Invariant tests are out there, but they don’t exactly make it easy to find the minimal failing traces. This can turn repro into a time sink, especially for those tricky liquidity pool invariants that only fail after a bunch of steps. Foundry v1.0 revamped the shrinking process and added some valuable invariant metrics. If you’re not on board yet, you might be missing out on savings. Get the scoop at (paradigm.xyz).
- Gas regressions can slip through the cracks. People often rely on makeshift reports or plugins, and many engineers shy away from running heavy test tasks on their machines. Luckily, there are now native gas snapshotting and diff gates available; just make sure you’re using them! You can learn more at (learnblockchain.cn).
- There’s a procurement and security risk with all this plugin sprawl in Node projects, unpinned viem type changes, and those recent supply-chain attacks targeting “extended” Hardhat toolboxes. This really bumps up the risk for your keys and CI runners--especially when you’re trying to scale up your contributors. More details can be found at (v2.hardhat.org).
Each of these adds real business risk and cost
- Missed deadlines: If your coverage and fuzz cycles are dragging, it's going to slow down merges significantly--think a 40% time penalty across your squad, which ends up being weeks instead of hours. When you look at the benchmarks for Foundry v1.0, it's clear it offers quicker compilation and better fuzz/coverage. Sticking with the old version? That's just leaving your ROI sitting on the floor. (paradigm.xyz)
- Launch risk: If your fork tests are flaky, they’re just hiding integration bugs with external AMMs, oracles, and L2 bridges. Hardhat’s forking and impersonation features are super handy, but without pinned blocks, custom headers, and reliable helpers, you can’t trust the test results. (hardhat.org)
- Audit cost: When you don’t have property-based invariants for the core invariants--like “totalAssets never decreases without withdrawal” and “oracle-quoted price monotonic within bounds”--auditors end up wasting time just trying to reproduce the basics.
- Supplier risk: Remember the npm supply-chain incident in 2025? It hit a viem-toolbox variant hard. If your CI is installing everything from scratch every time, that’s a recipe for disaster. You really can’t afford to skip locking down the official Hardhat toolboxes and pinning those viem types anymore. (advisories.gitlab.com)
- ZK/L2 edge cases: When it comes to zkEVM test harnesses, there are some limitations on cheat codes at call depth. If you're only testing based on L1 semantics, you’ll find out the hard way that your “harmless” little prank doesn’t actually hold up under zk constraints. (foundry-book.zksync.io)
A dual‑track testing architecture implemented with 7Block’s methodology
We’ve got a clear split in responsibilities: Foundry takes care of the Solidity-native correctness, fuzzing, invariants, and gas regressions, while Hardhat handles TypeScript/viem for end-to-end scenarios, deployment recipes, and making things easy for enterprise procurement.
1) Foundry for Solidity‑native speed, invariants, and gas guarantees
What We Implement:
- User-Friendly Interfaces: We focus on creating interfaces that are easy to navigate and pleasing to the eye, so you can find what you need without any hassle.
- Robust Security Measures: Keeping your data safe is our top priority. We use advanced security protocols to protect your information from unauthorized access.
- Seamless Integrations: Our systems work well with other tools you might already be using. We make it easy to connect and streamline your workflows.
- Regular Updates: We’re committed to keeping everything fresh. Expect regular updates to improve features and fix any bugs that pop up.
- Customer Support: Our support team is here for you! Whether you have a question or need assistance, we’ve got your back.
- Scalability: As your needs grow, our solutions can expand with you. We’re built to handle everything from small projects to large enterprises.
Feel free to reach out if you want to dive deeper into any of these points!
- We've rolled out Foundry v1.0, which comes packed with baseline features like invariant metrics and replayable failures. This means you can tackle those pesky bugs head-on instead of just debugging the framework. Our benchmarks reveal a solid 2x improvement compared to v0.2, with quicker coverage and enhanced invariant shrinking. Check it out here: (paradigm.xyz).
- Testing just got way more reliable with our deterministic mainnet forks. You can use cheatcodes to create, select, and roll multiple forks--pinning block numbers and strategically rolling to interact with oracle windows or liquidity shifts. Best part? No leaking state between forks! Dive into the details: (learnblockchain.cn).
- We’ve also introduced gas regression gating. With Forge’s native snapshot file, our CI’s “check/diff” feature can now block merges if any regressions exceed our tolerance. And guess what? No need for custom plugins! Learn more here: (learnblockchain.cn).
- Our compiler pipeline is now finely tuned for “gas per op” targets. We evaluate via-IR for each module, leading to solid gas improvements. With the guidance from Solidity’s 2024 update, via-IR is on course to become the norm, often delivering better gas efficiency while minimizing stack-too-deep issues. We’ve locked profiles for coverage versus deploy builds to keep everything balanced. More info here: (soliditylang.org).
- And finally, we’re evolving EVM testing! You can enable Prague/Pectra features (like EIP-7702) ahead of time to check out account abstraction interactions before they go live on L1. This helps avoid any last-minute surprises with keeper or sponsor flows. Get the scoop here: (paradigm.xyz).
Practical Foundry Example: Stateful Invariant with a Handler (ERC-4626 Vault Safety)
In this example, we’ll dive into creating a stateful invariant that checks the safety of an ERC-4626 vault. It’s all about ensuring that our vaults are operating securely and efficiently.
Setting Up the Environment
First things first, let’s make sure we have our environment set up. You’ll need to have Foundry installed. If you haven’t done that yet, you can follow the Foundry installation guide.
Once you’ve got Foundry ready to go, start by creating a new project:
forge init vault-safety
cd vault-safety
Writing the Smart Contract
Now, let's create a simple ERC-4626 vault contract. You can place this code in src/Vault.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
contract Vault is ERC20Burnable {
IERC20 public asset;
constructor(IERC20 _asset) ERC20("Vault Token", "VT") {
asset = _asset;
}
function deposit(uint256 amount) public {
asset.transferFrom(msg.sender, address(this), amount);
_mint(msg.sender, amount);
}
function withdraw(uint256 amount) public {
_burn(msg.sender, amount);
asset.transfer(msg.sender, amount);
}
}
Creating the Invariant
Next up, let’s create an invariant to ensure that the total amount of assets held by the vault matches what's been minted as vault tokens. You’ll add this to src/VaultInvariant.sol:
pragma solidity ^0.8.0;
import "./Vault.sol";
contract VaultInvariant {
Vault public vault;
constructor(Vault _vault) {
vault = _vault;
}
function invariant() public view {
require(vault.totalSupply() == vault.asset().balanceOf(address(vault)), "Invariant violated!");
}
}
Testing the Invariant
Now, we need to set up some tests to verify our invariant. Create a test file at test/VaultTest.t.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
import "../src/Vault.sol";
import "../src/VaultInvariant.sol";
contract VaultTest is Test {
Vault vault;
VaultInvariant invariant;
function setUp() public {
IERC20 asset = new ERC20("Asset Token", "AT");
vault = new Vault(asset);
invariant = new VaultInvariant(vault);
}
function testInvariant() public {
// Initial state
vault.deposit(100);
invariant.invariant();
vault.withdraw(50);
invariant.invariant();
}
}
Running the Tests
Finally, let’s run our tests and check if everything’s working as expected. Use the following command:
forge test
If all goes well, you should see that your invariant holds true, and your vault is safe!
Conclusion
And there you have it! You’ve successfully created a stateful invariant for an ERC-4626 vault, ensuring its assets are properly tracked. This provides a great safety net while working with vaults in smart contracts. If you have any questions or want to dive deeper into any part, feel free to ask!
// 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 takes advantage of Foundry’s invariant runner, handler targeting, and fork pinning to check the real deployed vault math while stateful fuzzing is in action. The shrinking process yields just a few minimal failing sequences whenever the invariants break. (learnblockchain.cn)
Gas Regression Gate (CI):
The Gas Regression Gate (CI) is a crucial component in assessing the reliability of gas wells. This gate helps in identifying whether a well has maintained its production capabilities over time. Here’s a quick rundown of how it works:
Purpose:
The Gas Regression Gate aims to:
- Evaluate production trends: It allows operators to monitor how gas production changes over time.
- Identify potential issues: By analyzing production data, it can help spot problems before they escalate.
Key Metrics:
When working with the Gas Regression Gate, keep an eye on these important metrics:
- Production Rate: The amount of gas being produced over a specific period.
- Decline Rate: How fast production is decreasing, which can indicate issues with the well.
- Cumulative Production: Total gas produced since the start of operations, giving a clear picture of overall performance.
How to Implement:
Setting up a Gas Regression Gate involves a few steps:
- Data Collection: Gather production data consistently to ensure accurate analysis.
- Analysis Tools: Utilize software tools or models that can process and visualize this data effectively.
- Regular Monitoring: Keep an eye on trends and report any significant changes to your team.
For more detailed insights, you can check out this resource. It’s packed with useful information on gas production trends and analysis techniques!
Conclusion:
Overall, the Gas Regression Gate (CI) is vital for maintaining the efficiency of gas production operations. By regularly monitoring and analyzing key metrics, operators can ensure their wells remain productive and address any issues before they become major headaches.
# Generate or update baseline
forge snapshot --snap .gas-snapshot
# Block merges if gas changed beyond 2%
forge snapshot --check --tolerance 2
Foundry creates deterministic gas snapshots that you can easily compare in your CI pipeline. The key takeaway here is: catch gas regressions before they impact TVL. Check out the details here.
2) Hardhat for realistic E2E flows, deployment orchestration, and procurement hygiene
What We Implement:
Here's a quick rundown of what we actually put into action:
- Hardhat 3 now rocks both Solidity tests and TypeScript (node:test) harnesses! This means Solidity tests deliver some awesome Foundry-style speed and even fuzz testing right within Hardhat. On the other hand, TypeScript tests help us mimic real user actions like handling multiple transactions, tracking events, and managing time. Plus, with built-in coverage in Hardhat 3, you can say goodbye to plugin maintenance headaches. (hardhat.org)
- Check out the Viem-based toolbox tailored for lightweight, typed clients. It’s designed to keep things stable by pinning viem, which is a tip straight from the Nomic Foundation’s docs--seriously, they recommend it to prevent any type-related mess. (v2.hardhat.org)
- Want to do some mainnet forking? Hardhat has you covered with deterministic mainnet forking that includes features like block pinning, custom headers, and impersonation. You also get handy helpers for setting balances, manipulating time, and adjusting gas--all in just one command in CI. How cool is that? (hardhat.org)
- When it comes to deployment, you can use Ignition modules (or stick with your current deployment setup) to keep everything consolidated and versioned. Just a heads-up: the Ignition package has moved, and so has its repo lifecycle--make sure to keep your versions managed within the Hardhat 3 toolboxes. (hardhat.org)
Practical Hardhat Example: End-to-End Swap on a Fork with Viem and Impersonation
In this guide, we'll walk through how to do an end-to-end swap on a fork using Hardhat, Viem, and impersonation. This approach will let us simulate transactions on a forked network while acting as a specific account. Let's dive in!
Prerequisites
Before you get started, make sure you have the following installed:
- Node.js (version 14.x or later)
- npm (comes with Node.js)
- Hardhat
You’ll also want to set up your Hardhat project if you haven’t already:
mkdir my-hardhat-project
cd my-hardhat-project
npm init -y
npm install --save-dev hardhat
npx hardhat
Follow the prompts to create a basic project setup.
Setting Up the Environment
First, we'll need to install some essential packages:
npm install --save-dev @nomiclabs/hardhat-ethers ethers viem
Next, let’s configure Hardhat to use a forking network. Open up hardhat.config.js and set it up like this:
require('@nomiclabs/hardhat-ethers');
module.exports = {
solidity: "0.8.0",
networks: {
hardhat: {
chainId: 1337,
forking: {
url: 'https://eth-mainnet.alchemyapi.io/v2/YOUR_ALCHEMY_API_KEY',
blockNumber: 12345678 // You can specify a block number to fork from
}
},
},
};
Make sure to replace YOUR_ALCHEMY_API_KEY with your actual Alchemy API key. This will enable us to fork from the Ethereum mainnet.
Using Viem and Impersonation
Now, let’s get into the script that makes this all work. Create a new file named swap.js in your scripts folder. Here’s what you’ll want it to look like:
const { ethers } = require("hardhat");
const { createClient } = require("viem");
const { mainnet } = require("viem/chains");
async function main() {
// Start by creating a Viem client
const client = createClient({
autoConnect: true,
chain: mainnet,
});
// Set the address you want to impersonate
const addressToImpersonate = "0xYourAddress"; // Replace with the address you want to impersonate
// Impersonate the account
await hre.network.provider.send("hardhat_impersonateAccount", [addressToImpersonate]);
// Get the signer for the impersonated account
const signer = await ethers.getSigner(addressToImpersonate);
// Now you can interact with contracts as this account
const token = await ethers.getContractAt("IERC20", "0xTokenAddress", signer); // Replace with the token contract address
// Here’s where you would perform the swap
const amount = ethers.utils.parseUnits("1.0", 18); // Amount of tokens to swap
const tx = await token.transfer("0xRecipientAddress", amount); // Replace with recipient's address
await tx.wait();
console.log(`Successfully transferred ${amount.toString()} tokens!`);
}
// Execute the main function
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
Make sure to replace 0xYourAddress, 0xTokenAddress, and 0xRecipientAddress with the actual addresses you're working with.
Running the Script
Once you've got everything set up, it’s time to run your script. In your terminal, execute:
npx hardhat run scripts/swap.js --network hardhat
If everything goes well, you should see a confirmation message in your console telling you that the transfer went through like a breeze.
Conclusion
And there you have it! You've successfully performed an end-to-end swap on a forked network using Hardhat, Viem, and impersonation. Feel free to customize the script or expand upon it as you explore more functionalities. Happy coding!
// 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 it super easy to reproduce cross-protocol flows. Plus, viem keeps the client lightweight and typed. Don’t forget to pin the fork block to avoid any drift and CI issues. Check it out here: (hardhat.org).
Built-in Solidity Test with Coverage (Hardhat 3)
Hardhat 3 makes testing your Solidity smart contracts super convenient, and it even has built-in tools for measuring coverage. Here's how you can get started with it:
Setting Up
To kick things off, make sure you have a Hardhat project. If you haven’t created one yet, just run:
npx hardhat
Follow the prompts to set everything up. Once you’re good to go, you'll want to add the Hardhat test coverage plugin:
npm install --save-dev hardhat-plugin-coverage
Running Tests
Now that you’ve got everything installed, you can write your tests. If you haven't written any tests yet, check out some basic examples:
const { expect } = require("chai");
describe("MyContract", function () {
it("Should return the correct value", async function () {
const MyContract = await ethers.getContractFactory("MyContract");
const myContract = await MyContract.deploy();
expect(await myContract.someMethod()).to.equal("Expected Result");
});
});
Save your test files in the test directory, and when you're ready to run them, just execute:
npx hardhat test
Checking Coverage
Once you've run your tests, it’s time to check how much of your code is covered. Simply run:
npx hardhat coverage
This will generate a coverage report, giving you a clear insight into which parts of your code are being tested and which aren't.
Viewing the Report
You can find the coverage report in the coverage directory that gets created. Open the index.html file in your browser to see a detailed overview of your test coverage.
Conclusion
With all these tools at your disposal, testing your Solidity contracts becomes a breeze. Hardhat really streamlines the process, making sure you’ve got everything covered. Happy coding!
# Run only Solidity tests with coverage in HH3
npx hardhat test --coverage --group solidity
Hardhat 3 brings in native coverage, so you can say goodbye to the hassle of managing coverage plugins. The key takeaway? Fewer moving parts and fewer supply-chain nodes. Check it out here: (hardhat.org)
3) ZK/L2‑aware testing
- When it comes to zkEVM (like zkSync), there are some cheatcodes that need to run at the root of the test context, not tucked away in nested calls. We've got your back on this! We pinpoint those instances and replicate quirky stuff (like timestamps, gas metering, and precompiles) in a special profile to make sure your invariants stay consistent between L1 and ZK. Check it out here: (foundry-book.zksync.io)
- For OP‑stack or any chains that have different gas and refund rules, Hardhat 3’s multichain simulation is here to help. It makes TypeScript tests line up better with what’s actually happening on-chain. Take a look: (hardhat.org)
When to use which (short version)
- Go for Foundry when you need accuracy, speed, and low gas fees: think fuzzing, checking invariants, figuring out “why did this revert?”, gas differences, and running a whopping 10k test cases quickly. With the release of Foundry v1.0, we got some cool updates like quicker coverage and shrinking invariants, better internal decoding, the ability to replay failures, and nifty gas snapshot cheatcodes. Check it out: (paradigm.xyz).
- Choose Hardhat when it’s all about orchestration and end-to-end processes: this is where you want user-like transaction sequences, solid deployment plans, explorer verification, and a Node/TypeScript setup that’s friendly for procurement. Hardhat 3 brings some awesome features like Solidity tests, the viem toolbox, and built-in coverage. Dive into it here: (hardhat.org).
We're merging the best of both worlds here: using Foundry for our inner loops with continuous integration (CI) on every pull request, and Hardhat for the outer loops, which includes nightly end-to-end tests on forked networks along with deployment rehearsals.
Implementation blueprint (what we set up for you)
1) Profiles and Build Matrix
- We've got Foundry profiles set up for testing, coverage, and deployment. The via-IR feature is selectively turned on for each package, complete with branch-specific gates. Also, with Solidity 0.8.28, there are some cool improvements to IR and transient storage that are super helpful for advanced accounting patterns. Check it out here: (soliditylang.org)
- On the Hardhat front, version 3 is rolling with a new config that features declarative build profiles and typed artifacts for TypeScript tests. Dive into the details here: (hardhat.org)
2) Deterministic Forks and Helpers
- Foundry: You can use
createSelectFork,rollFork, and the explicitvm.deal/prank/warpfunctions to keep your invariants consistent. Plus, you can manage multiple chain forks in a single file, which is super handy for cross-chain adapters. If you want to dive deeper, check it out here. - Hardhat: With Hardhat, you can fork the mainnet using a pinned block and archive RPC. They’ve got tools like
getImpersonatedSignerfor balance manipulation and custom headers for authentication. For more details, have a look at their guide here.
3) Gas Optimization Guardrails
- Use
forge snapshot --check --tolerance Nalong with “gas budget” thresholds for each module. Key takeaway: these automatic gas gates help stop those sneaky increases in unit costs. (learnblockchain.cn)
4) Invariant library and metrics
- We’ve got some solid canonical invariants for vaults, AMMs, and liquidations, like solvency, monotonicity, and fee accounting. With Foundry v1.0, you can dig into the invariant metrics, which will show you call counts and discard rates to help prove coverage depth. Check it out here: (paradigm.xyz)
5) Coverage Everywhere
- For Foundry, you can run
forge coverage --report lcov, and then check out the VSCode gutters along with the LCOV HTML report. This lets reviewers spot any uncovered lines right in PRs. If you want more details, check it out here: (rareskills.io). - As for Hardhat 3, just toss in
--coverage--no need for extra plugins, which makes CI a breeze. Learn more about it here: (hardhat.org).
6) Procurement and Supply-Chain Hygiene
- Stick to the official toolboxes; steer clear of those “extended” versions that got compromised back in 2025. If you’ve installed anything suspicious, make sure to rotate your keys. Keep an eye on viem since it tends to change a lot. Key takeaway: reduce blast radius in CI. (advisories.gitlab.com)
7) ZK Profiles
- You can set up a specific zkSync Foundry profile that comes with root-level cheatcode discipline. Just keep in mind the documented limitations, and you’ll also get mirrored test semantics, making sure your results align perfectly with zkEVM. Check it out here: (foundry-book.zksync.io)
Proof: GTM‑relevant metrics you can expect
- Faster CI feedback loops: With Foundry v1.0, you can expect up to 40% quicker fuzz and coverage execution, plus a whopping 3.5x speedup in coverage during benchmarks. This means fewer blocked PRs and a nice drop in CI minutes. Check it out here.
- Lower integration risk: Hardhat’s built-in coverage, along with the viem toolbox and fork pinning, tackle plugin mismatches and semver surprises. Plus, Nomic Foundation recommends viem pinning, so you're in good hands. Find more info here.
- Gas cost discipline: Thanks to native snapshots and diff-gated PRs, you can catch regressions early, preventing them from messing with user economics. More details are available here.
- Flake reduction: By using fork pinning and impersonation helpers, you’ll see a real drop in test nondeterminism. Hardhat's documentation suggests using pinned block numbers and archive RPCs for even more reliability. Check it out here.
- Security posture: With a reduced surface area for third-party plugins (thanks to built-in coverage and official toolboxes) and allow-listed dependencies, you can steer clear of incidents like the 2025 npm compromise. Read more about it here.
Practical patterns you can copy‑paste today
- Running a Foundry multi-fork test to check out AMM quotes across different chains.
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 during a test helps keep the state separate while still maintaining the test contract variables--perfect for those cross-chain adapters. (learnblockchain.cn)
2) Hardhat Solidity Tests Alongside TS Tests (HH3)
When it comes to testing your smart contracts, combining Hardhat's Solidity tests with TypeScript (TS) tests can really boost the robustness of your development process. Here’s how to set it up and get the best of both worlds.
Setting Up Your Environment
First off, make sure you have Hardhat installed and ready to roll. If you haven't done that yet, you can get started by running:
npm install --save-dev hardhat
Once you’ve got Hardhat in your project, you can create a new Hardhat project by running:
npx hardhat
Follow the prompts to set everything up just the way you like it.
Writing Solidity Tests
Now, let’s dive into writing some tests in Solidity! Head over to your test directory and create a new file, say MyContract.test.js. Here’s a quick example of what your Solidity test might look like:
pragma solidity ^0.8.0;
import "hardhat/console.sol";
import "../contracts/MyContract.sol";
import "forge-std/Test.sol";
contract MyContractTest is Test {
MyContract myContract;
function setUp() public {
myContract = new MyContract();
}
function testInitialValues() public {
assertEq(myContract.value(), 0);
}
}
This snippet sets up your contract and tests an initial value. Pretty neat, right?
Writing TypeScript Tests
Next up, let’s add some TypeScript tests to the mix! Create a new file like myContract.test.ts in the same test folder. Here’s a simple example to get you started:
import { ethers } from "hardhat";
import { expect } from "chai";
describe("MyContract", function () {
let myContract: any;
beforeEach(async function () {
const MyContract = await ethers.getContractFactory("MyContract");
myContract = await MyContract.deploy();
await myContract.deployed();
});
it("should have an initial value of 0", async function () {
expect(await myContract.value()).to.equal(0);
});
});
This TypeScript test checks the same initial value as the Solidity test but uses the Chai assertion library for a more fluent style.
Running Your Tests
To run your tests, just use the Hardhat command:
npx hardhat test
This will execute all your tests--both Solidity and TypeScript--sublimely.
Conclusion
By merging Hardhat’s Solidity tests with TypeScript tests, you’ll ensure that your smart contracts are well-tested and your application logic is solid. This dual approach helps catch issues early and can make your life a whole lot easier as you build and maintain your decentralized applications. Happy coding!
- Keep your
.t.solfiles right next to the contracts for those unit-level Solidity tests. - For TypeScript flows, go with
npx hardhat test nodejs, and for everything else, just runnpx hardhat test. Don’t forget to throw in--coveragewhen you need it. (hardhat.org)
3) Coverage Visualization for Reviewers (Foundry)
When it comes to reviewing code, having a clear picture of coverage is super important. Foundry makes it easy to visualize this coverage, helping reviewers quickly spot the areas that need attention.
Here’s what you can expect:
- Interactive Graphs: Get a quick view of how much of your code is covered by tests. The graphs are interactive, so you can dive deeper into specific areas.
- Color-Coded Metrics: Easily identify what's well-tested versus what might be lacking. Green for good coverage and red for potential trouble spots.
- Detailed Reports: Need more info? You can pull up detailed reports that dissect coverage down to the file and line level. This way, you can understand exactly where you might need to add tests.
For more information, check out the Foundry Documentation.
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
Check it out here: (rareskills.io)
4) Dealing with Flaky Forks (Anvil/HH Network)
When it comes to managing flaky forks, especially in the context of Anvil and the HH Network, there are a few strategies you can keep in mind to make your life a bit easier.
Understanding Flaky Forks
Flaky forks can be a real headache. They often pop up when you least expect them, causing confusion and delays in your projects. So, what exactly are they? Essentially, these are branches in your code that behave unpredictably, sometimes working fine in one instance and then failing in another.
Key Strategies for Handling Flaky Forks
Here are some handy tips to help you tackle these unruly forks:
- Regular Updates: Keep your forks up to date with the main branch. This helps to minimize the differences that could cause flaky behavior.
- Automated Testing: Implement a solid suite of automated tests. This will help catch flaky behavior early in the development process.
- Clear Documentation: Document any issues you encounter with flaky forks. This not only helps you keep track of problems but also aids anyone else who might work with that code later.
- Isolate Changes: When you suspect a fork is going flaky due to specific changes, isolate those changes to pinpoint the issue more easily.
- Collaborate with Your Team: Don’t hesitate to reach out to your teammates for help. Sometimes a fresh pair of eyes can see things you’ve missed.
Additional Resources
For a deeper dive into managing flaky forks, you might want to check out the following resources:
By following these strategies, you can make dealing with flaky forks a little less daunting and keep your projects running smoothly!
- Make sure to use
--fork-urland set block numbers; cache those RPC responses for repeated runs; and opt for top-notch archive RPCs to steer clear of timeouts during heavy traces. (getfoundry.sh)
Tooling notes that save hours (and RPC credits)
- Foundry logging/traces: To get a deep dive into what’s going on behind the scenes, try running
forge test -vvvalong with the--flamegraphoption. This combo will show you internal calls and hotspots, plus you can replay any failing tests in a consistent way. Check it out more here: (paradigm.xyz). - Anvil modes: Using interval or on-demand mining can really up the realism for keeper flows. Plus, make sure to preinstall a CREATE2 deployer so you can get those addresses consistently. More details can be found here: (getfoundry.sh).
- Solidity compiler pipeline: Keep an eye on those via-IR improvements! Since versions 0.8.26 and 0.8.28, the optimizations and new features (like custom errors in require statements and transient storage) have made IR pipelines a lot more appealing for production builds. Always remember to profile instead of just guessing! Dive into it here: (soliditylang.org).
How 7Block Labs executes (and why it pays back quickly)
- We’ve got a dual-track CI system in place: there’s a Foundry fast lane for each PR, complete with gas and invariant checks, plus a nightly Hardhat end-to-end suite that runs on both forked mainnet and your target L2s. This way, you get quick feedback where it really matters and a realistic testing environment where it counts.
- We streamline the deployment process using Hardhat Ignition modules or whatever deployment setup you prefer, making sure verification, tagging, and rollback points are all wired in.
- We create invariants that tie directly to business KPIs like solvency, fee capture, and slippage limits. This means that when you see “green tests,” it actually reflects the safety of your TVL, not just how many bytes you saved.
- We work hand-in-hand with your auditors by crafting invariants and property tests that align with their preferred patterns, along with providing coverage reports that fit neatly into their workflows.
You can totally enhance your project with our follow-on services if you need them:
- Get full protocol builds with our DeFi development services and smart contract development.
- Boost your security with our security audit services and gas-focused reviews.
- Enjoy seamless composability and integrations through our dApp development and cross-chain solutions.
- If you’re just starting to scope your build, our blockchain development services and web3 development services cover everything from start to finish.
Bottom line
- Leverage Foundry for what it does best: think invariants, fuzzing, gas snapshots, decoded traces, and speedy iteration--now even quicker in v1.0! Check it out here: (paradigm.xyz)
- Turn to Hardhat for its strengths: we’re talking TypeScript/viem E2E, deterministic forks, and handy built-in coverage that fits right into your procurement plans. Plus, you can pin and audit your dependencies easily. More details here: (hardhat.org)
- Tie it all together with some discipline: think pinned blocks, profiles, CI gates, and keeping a clean supply chain.
7Block Labs can get this up and running in just a few days--not months--and link it to real, measurable ROI: think fewer flaky builds, quicker merges, earlier catches of issues, and enforced gas budgets.
Looking to simplify your launch and accelerate your roadmap?
Book a DeFi Testing Strategy Call
Dive into the world of decentralized finance! If you're looking to enhance your understanding or need help with testing strategies, let’s chat.
Whether you're just starting out or you're knee-deep in projects, having a solid plan is crucial. We can explore your goals, challenges, and tailor a strategy that works for you.
Don't hesitate to reach out--I'm here to help!
Like what you're reading? Let's build together.
Get a free 30-minute consultation with our engineering team.
Related Posts
ByAUJay
Revamping Old Solidity Code for Today's Standards
Refactoring old Solidity contracts from versions 0.5-0.7 to the latest 0.8.x isn’t just a luxury anymore; it’s a must-have for Enterprise teams looking to save on Layer 2 costs, achieve SOC2 compliance, and reduce risks when upgrading. By using ERC-7201 namespaced storage, EIP-1153 transient storage, and OZ 5.x libraries, teams can streamline their processes and enhance their contracts significantly.
ByAUJay
A Simple Guide to Auditing Your Code Before You Send It Off to a Firm
**Summary:** This is a hands-on, engineering-focused guide that helps you do a self-check on your Solidity and ZK code, so you can kick off your external audit right from day one instead of waiting until week four. It's tailored for enterprise teams that need to balance secure-by-design practices with the requirements of SOC2 compliance.
ByAUJay
Why Your dApp Frontend is Slow (and How to Fix RPC Bottlenecks)
**Summary:** A lot of the “slow dApps” you hear about aren't just issues with the front-end; they’re actually tied to RPC bottlenecks. In this post, we’ll dive into where latency sneaks in (think methods, limits, and clients), what it’s costing your DeFi product in terms of failed sessions and RPC expenses, and how 7Block Labs can help you tackle these challenges.

