Development Tooling
Hardhat and Foundry, tests that fork mainnet, the libraries your front end talks through, and where your RPC actually goes.
You Cannot Debug In Production
A working setup gives you four things: a local chain that mines instantly, a test suite that can fork real mainnet state, a way to talk to contracts from an application, and a reliable node connection. Everything below is one of those four.
Hardhat & Foundry
Two frameworks dominate, and the split is about the language your tests are written in.
| Hardhat | Foundry | |
|---|---|---|
| Tests written in | JavaScript / TypeScript | Solidity |
| Speed | Fine | Very fast — it is Rust underneath |
| Fuzzing | Via plugins | Built in, first class |
| Best when | Your app and tests share a language | You want depth and speed in the contract layer |
Both give you a local node that mines on demand, funded accounts, time manipulation and state snapshots. Teams increasingly run both: Foundry for contract-level testing and fuzzing, Hardhat for deployment scripts and integration with a TypeScript front end.
Testing Contracts
Contract tests carry unusual weight, because the alternative to catching a bug in CI is catching it in an incident report. Four layers are worth having.
| Layer | Catches |
|---|---|
| Unit tests | Logic, access control, revert conditions |
| Fuzz tests | Inputs you never thought of — random values against an invariant |
| Invariant tests | Properties that must hold after any sequence of calls |
| Fork tests | Integration against real deployed protocols and real state |
// Foundry: a fuzz test. The runner supplies hundreds of random amounts.
function testFuzz_DepositThenWithdraw(uint96 amount) public {
vm.assume(amount > 0);
vm.deal(alice, amount);
vm.prank(alice);
escrow.deposit{value: amount}();
assertEq(escrow.balanceOf(alice), amount);
}
Invariants are where the real bugs surface: total supply equals the sum of balances, the contract never holds less than it owes, no user can withdraw more than they deposited. State them explicitly and let the fuzzer attack them.
web3.js & ethers.js
Your application talks to the chain through a library that encodes calls, signs transactions and decodes results. ethers and the newer viem are the current defaults; web3.js was the original and is now mostly legacy.
import { ethers } from "ethers";
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const escrow = new ethers.Contract(address, abi, signer);
// read: free, no wallet prompt
const balance = await escrow.balanceOf(await signer.getAddress());
// write: costs gas, prompts the wallet, returns before it is mined
const tx = await escrow.deposit({ value: ethers.parseEther("0.1") });
await tx.wait(); // now it is on-chain
The distinction that trips people up: a provider reads and a signer writes. Reads are free and instant. Writes cost gas, need user approval, and return a pending transaction — the state has not changed until wait() resolves, and it may still revert.
Nodes & RPC Providers
Every read and every broadcast goes through a node. You either run one or pay someone who does, and the choice has consequences beyond convenience.
| Option | Trade-off |
|---|---|
| Your own node | No trust, no rate limit — terabytes of storage and real operations |
| Hosted RPC provider | Instant and reliable — a third party sees every query you make |
| Public endpoint | Free, rate-limited, unsuitable for production |
Know what you are asking for, too. An archive node keeps every historical state and is what fork tests and analytics need; a full node prunes old state and is cheaper. Many "node down" bugs are really a call that needed archive data hitting a pruned node.
Interview Questions
Hardhat or Foundry?
Foundry writes tests in Solidity with built-in fuzzing and is much faster; Hardhat writes them in TypeScript and integrates naturally with a JS front end. Many teams run Foundry for contract tests and Hardhat for deployment.
What is an invariant test?
A property that must hold after any sequence of calls — total supply equals the sum of balances, say — checked by a fuzzer that generates call sequences trying to break it.
Why fork mainnet in tests?
It runs your contract against real deployed protocols and real state at a pinned block, which is the only way to check integration assumptions before they cost money.
Provider versus signer?
A provider reads chain state — free and instant. A signer can send transactions, which cost gas, require user approval and are not final until mined.
Why does awaiting a transaction not mean it succeeded?
The call returns once the transaction is broadcast. It may still be pending, replaced, or revert on execution — only the mined receipt tells you the outcome.
What is the risk of a hosted RPC provider?
It is a centralised dependency that can rate-limit, serve stale data, censor broadcasts or fail, and it observes every address your users query. Production setups keep a fallback.