Ethereum development frameworks: the good, bad, and broken

February 08, 2025 / 10 min read / - views​

I’ve shipped production contracts with all four: Hardhat, Foundry, Truffle, and Brownie. Some weeks it felt like speed‑running the entire history of Ethereum tooling: CoffeeScript ghosts, Rust EVMs, and Python test bliss held together with duct tape. This post is a straight answer to “which one should I use in 2025?” — with real configs, code, and the rough edges I actually hit.


TL;DR scorecard

FrameworkWho it’s for in 2025StrengthsWeak spotsState
Hardhat (v3)TypeScript teams, multi‑chain dapps, plugin‑heavy stacksMature plugin ecosystem, great stack traces, new Rust runtime is fast, first‑class Solidityand TS tests, Ignition deploymentsConfig surface is big, Ignition mental model takes a minute, plugin bloat if you’re carelessActively maintained, recommended default
Foundry (v1+)Solidity‑first teams, auditors, gas nerds, CI minimalistsRidiculous speed, cheatcodes, fuzz/property testing, gas snapshots, Anvil fork devnets, Solidity scriptsFrontend bindings/types are DIY, coverage can be fussy across edge cases, fewer “turnkey” pluginsActively maintained, often fastest path
TruffleLegacy projects onlyFamiliar migrations, huge amount of old docs/examplesSunset/archived; no forward path for new workSunset: migrate
BrowniePython shops, researchers, data folks who love pytestLovely Python ergonomics, readable tests, solid scriptingMaintenance slowed; some devnet features incomplete; ecosystem moved to ApeMaintenance‑light: consider Ape or mix with Anvil

My short answer: pick Hardhat if you live in TypeScript land or need plugins; pick Foundry if you live in Solidity and care about speed and test rigor. Keep Truffle for archaeology. Use Brownie only if Python testing is the hill you’re dying on, and pair it with Anvil.


How I’m judging

  • DX: setup friction, error quality, docs, plugin surface
  • Tests: speed, Solidity vs JS/TS, fuzzing, fixtures, coverage
  • Debugging: stack traces, console.log, traces, local forks
  • Deployments: first‑party deploy UX, verification, secrets management
  • Ecosystem health: maintenance pace, community, integrations
  • Multi‑chain: rollup realism, forking reliability, Base/OP Stack, etc.

This isn’t a lab benchmark; it’s a field report. Where I cite bugs or footguns, they were either mine, my team’s, or I ran into them in the wild and reproduced.


Hardhat (v3): the everything‑bagel that finally got fast

Hardhat 3 ships a Rust EVM runtime under the hood and adds Solidity tests next to your usual TypeScript tests. The experience: fewer “why did this revert?” moments, better stack traces, and a stable of official plugins so you don’t end up cobbling.

What I love

  • Batteries included: Toolbox for testing, coverage, verification; official plugins for Viem or Ethers.
  • Solidity + TS tests: unit tests in Solidity for speed, integration tests in TS for expressiveness.
  • Great errors: actionable revert messages with Solidity stack traces.
  • Ignition deployments: declarative modules, repeatable, verifiable.
  • Multi‑chain realism: mainnet/rollup forking that behaves.

What bites

  • Ignition’s learning curve: the module mental model is powerful but not “five‑minute deploy script” simple.
  • Plugin sprawl: easy to over‑install and slow CI; be intentional.
  • Config surface: flexible means verbose. Keep your hardhat.config.ts tidy.

Minimal project setup (Hardhat 3 + Ethers)

npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox-mocha-ethers typescript ts-node
npx hardhat init

hardhat.config.ts

import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox-mocha-ethers";
import "@nomicfoundation/hardhat-verify";
 
const config: HardhatUserConfig = {
  solidity: {
    version: "0.8.26",
    settings: { optimizer: { enabled: true, runs: 200 } },
  },
  networks: {
    hardhat: { forking: process.env.MAINNET_RPC ? { url: process.env.MAINNET_RPC } : undefined },
    sepolia: { url: process.env.SEPOLIA_RPC || "", accounts: process.env.PRIV_KEY ? [process.env.PRIV_KEY] : [] },
  },
  etherscan: { apiKey: process.env.ETHERSCAN_KEY || "" },
};
export default config;

A tiny Solidity unit test using Hardhat’s Solidity Test runner:

contracts/Counter.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
 
contract Counter { uint256 public n; function inc() external { n++; } }

test/Counter.t.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import {Test} from "hardhat/console.sol"; // placeholder import for example only
 
contract CounterTest {
    Counter c;
    function setUp() public { c = new Counter(); }
    function testInc() public { c.inc(); require(c.n() == 1, "bad"); }
}

TypeScript integration test:

test/counter.spec.ts

import { expect } from "chai";
import { ethers } from "hardhat";
 
describe("Counter", function () {
  it("increments", async () => {
    const Counter = await ethers.getContractFactory("Counter");
    const c = await Counter.deploy();
    await c.waitForDeployment();
    await (await c.inc()).wait();
    expect(await c.n()).to.equal(1n);
  });
});

Ignition deployment module:

ignition/modules/Counter.ts

import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";
 
export default buildModule("CounterModule", (m) => {
  const counter = m.contract("Counter");
  return { counter };
});

Deploy and verify:

npx hardhat ignition deploy ignition/modules/Counter.ts --network sepolia
npx hardhat verify --network sepolia <DEPLOYED_ADDRESS>

When to pick Hardhat

  • You want first‑party deployments, verification, coverage, and a plugin for almost everything.
  • The team writes frontend in React/TS and wants shared types and testing ergonomics.
  • You need to simulate OP Stack/Base quirks locally.

Skip if your tests are all Solidity, you need the absolute fastest inner loop, and you don’t want Node in the toolchain.


Foundry: the fast lane for Solidity purists (and auditors)

Foundry is Rust under the hood, Solidity at the top. Tests, scripts, and fixtures all in Solidity; Anvil for local devnets and mainnet forking; Cast for a powerful CLI. It’s hard to beat for speed and for the quality of on‑chain style tests.

What I love

  • Speed: compile/test/fuzz cycles are snappy even on large repos.
  • Cheatcodes: warp time, prank senders, expect reverts, etc.
  • Gas discipline: snapshotGas and gas reports checked into git make reviews objective.
  • Fork‑first dev: anvil --fork-url ... with traces that make sense.
  • Solidity scripts: deployments that feel like tests, plus --broadcast when ready.

What bites

  • Frontend types/bindings: no official TypeChain story; you glue it yourself.
  • Coverage rough edges: reports can be patchy across certain patterns; treat coverage as directional.
  • Fewer turnkey plugins: you compose more from primitives.

Minimal project setup

curl -L https://foundry.paradigm.xyz | bash
foundryup
forge init foundry-demo && cd foundry-demo

foundry.toml

[profile.default]
solc_version = "0.8.26"
optimizer = true
optimizer_runs = 200
fs_permissions = [{ access = "read", path = "./" }]

src/Counter.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract Counter { uint256 public n; function inc() external { n++; } }

test/Counter.t.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import {Test, console2} from "forge-std/Test.sol";
import {Counter} from "src/Counter.sol";
 
contract CounterTest is Test {
    Counter c;
    function setUp() public { c = new Counter(); }
    function testInc() public { c.inc(); assertEq(c.n(), 1); }
    function testFuzzInc(uint8 x) public { vm.assume(x < 3); for (uint i; i < x; i++) c.inc(); assertEq(c.n(), x); }
}

A tiny Solidity deployment script:

scripts/Deploy.s.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import {Script} from "forge-std/Script.sol";
import {Counter} from "src/Counter.sol";
 
contract Deploy is Script {
    function run() external {
        vm.startBroadcast();
        new Counter();
        vm.stopBroadcast();
    }
}

Run it against a fork and get a gas report:

anvil --fork-url "$MAINNET_RPC" &
forge test -vvv
forge coverage --report lcov && genhtml lcov.info --output-dir coverage
forge script scripts/Deploy.s.sol --rpc-url $SEPOLIA_RPC --broadcast --verify --etherscan-api-key $ETHERSCAN_KEY

When to pick Foundry

  • You want Solidity‑first everything, from tests to deploy scripts and fuzzing.
  • You care about gas budgets and quick feedback loops.
  • You’re building libraries or audits where TypeScript adds no value.

Skip if your app is TypeScript‑heavy and you want ready‑made plugins, or you need polished, first‑party coverage/verification GUIs.


Truffle: thank you for your service

I cut my teeth on Truffle migrations and Ganache GUI. In 2025, Truffle is effectively history. If you’re still on it, do yourself a favour and migrate. Your future self will buy you a coffee.

Migration sketch

  • If you need a plugin ecosystem and TS tests: migrate to Hardhat.
  • If you want Solidity‑only tests and speed: migrate to Foundry.
  • Replace Ganache with Anvil or Hardhat Network.
  • Port migrations to Hardhat Ignition modules or Foundry scripts. Start with a single deploy, verify on testnet, then refactor the rest.

Brownie: Pythonic joy, thin ice maintenance

I love writing tests in pytest. Brownie gives you that, with clean fixtures and readable scripts. The tradeoff in 2025 is maintenance and ecosystem gravity.

What works well

  • Python tests and scripts that data teams actually grok.
  • Quick brownie console poking at contracts.
  • Can run against Anvil or Hardhat local networks for speed.

What to watch

  • Maintenance cadence is light; new chain features may lag.
  • Some devnet capabilities are incomplete compared to Anvil/Hardhat.
  • The Python ecosystem has largely consolidated around Ape for greenfield.

Minimal Brownie example using Anvil

pipx install eth-brownie
anvil --fork-url "$MAINNET_RPC" &
mkdir brownie-demo && cd brownie-demo && brownie init

contracts/Counter.sol (same as above)

scripts/deploy.py

from brownie import accounts, Counter
 
def main():
    acct = accounts[0]
    return Counter.deploy({"from": acct})

Run and test:

brownie run scripts/deploy.py --network anvil
brownie test --network anvil -s

Real‑world gotchas and fixes

  • Hardhat Ignition “why won’t it redeploy?” Clear the previous deployment state (.ignition folder) or use reconciliation options; idempotency is a feature, not a bug.
  • Foundry coverage confusion: treat numbers as approximate and focus on critical paths. Pair with Hardhat’s solidity-coverage if you need a second opinion in CI.
  • Anvil vs Brownie transaction metadata: don’t assume tx.return_value semantics match Ganache; assert on contract state instead.
  • Forking on rollups: beware L2 predeploys and system contracts. In Foundry, pass --fork-block-number when debugging non‑deterministic issues.
  • Secrets in CI: with Hardhat, prefer the keystore/secrets plugin + env files; with Foundry, pass API keys at the command line or use your CI’s secret manager.

Choosing in 60 seconds

  • You ship a React/TS dapp: Hardhat. Viem/Ethers plugins, TypeChain, Ignition, coverage, the works.
  • You ship a protocol/library or do audits: Foundry. Solidity‑native tests, fuzzing, Anvil forks, gas snapshots.
  • You’re stuck on Truffle: move. Pick Hardhat or Foundry based on the two bullets above.
  • You love Python tests: Brownie works, but evaluate Ape for greenfield and pair with Anvil.

My default stack in 2025

  • Protocol repos: Foundry for tests/scripts, Anvil for forks. Separate minimal Hardhat project only for coverage/verification if stakeholders insist on those artifacts. Works great in monorepos.
  • App repos: Hardhat 3 with Ethers or Viem toolbox, Ignition modules for deploys, OP Stack/Base simulation locally. Foundry layered in for fuzz and gas snapshots on critical contracts.

If you want to copy a battle‑tested layout, I keep a template with:

  • Foundry src/, test/, scripts/ and gas snapshots gated in CI.
  • Hardhat packages/tooling/ just for Ignition deploy + verify + coverage.
  • Shared ABIs and TypeChain bindings into the frontend app via a small build step.

Snippets I paste way too often

Foundry: forked mainnet dev session

anvil --fork-url $MAINNET_RPC --fork-block-number 21000000 &
cast block-number --rpc-url http://127.0.0.1:8545
forge test -vv

Foundry: gas snapshot in CI

forge test --gas-report > gas-report.txt
forge snapshot  # generates ./gas-snapshot

Hardhat: quick Base/OP local sim

// hardhat.config.ts
networks: {
  hardhat: {
    chainId: 31337,
    // keep state diffs small and deterministic for tests
    initialBaseFeePerGas: 0,
  },
}

Hardhat: verify right after Ignition deploy

ADDR=$(jq -r '.results[0].address' .ignition/deployments/sepolia/CounterModule.json)
npx hardhat verify --network sepolia $ADDR

My take

The 2025 split is healthy: Hardhat as the pragmatic, batteries‑included TypeScript platform; Foundry as the lean, high‑performance Solidity toolkit. You can mix them. I often do. Just don’t start a new project on Truffle, and be realistic about Brownie’s maintenance story. If you grew up on Python and want that DX, go kick the tyres on Ape.