Blockchain · Guide

Development Tooling

Hardhat and Foundry, tests that fork mainnet, the libraries your front end talks through, and where your RPC actually goes.

— min read Blockchain

You Cannot Debug In Production

Deployment is permanent and every mistake costs real money, so the local loop carries far more weight here than in ordinary development. The tooling exists to make failure happen on your machine.

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.

HardhatFoundry
Tests written inJavaScript / TypeScriptSolidity
SpeedFineVery fast — it is Rust underneath
FuzzingVia pluginsBuilt in, first class
Best whenYour app and tests share a languageYou 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.

LayerCatches
Unit testsLogic, access control, revert conditions
Fuzz testsInputs you never thought of — random values against an invariant
Invariant testsProperties that must hold after any sequence of calls
Fork testsIntegration 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.

Forking mainnet is the closest thing to a staging environment. Tests run against the real deployed protocols your contract integrates with, at a pinned block — which is how integration assumptions get checked before they cost anything.

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.

The ABI is how a library knows what a contract looks like. Ship it with your front end and keep it in step with the deployed bytecode — a stale ABI produces decoding errors that look like network faults.

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.

OptionTrade-off
Your own nodeNo trust, no rate limit — terabytes of storage and real operations
Hosted RPC providerInstant and reliable — a third party sees every query you make
Public endpointFree, rate-limited, unsuitable for production
A hosted provider is a centralisation point in an otherwise decentralised stack. It can rate-limit you, serve stale state, censor a broadcast, or simply go down — and it sees which addresses your users are asking about. Have a fallback endpoint.

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.

Quick Quiz

1. Foundry tests are written in…
2. A fuzz test supplies…
3. Reads from the chain require…
4. Fork tests against historical state need…
5. After `await tx` on a write, the state change is…