PluginBench
Skill
Review
Audit score 70

web3-testing

wshobson/agents

Test smart contracts comprehensively with Hardhat and Foundry—unit tests, integration tests, gas optimization, fuzzing, and mainnet forking.

What is web3-testing?

Comprehensive testing framework for Solidity smart contracts using Hardhat and Foundry. Use this skill when writing unit tests, setting up integration test suites, performing gas optimization testing, fuzzing for edge cases, forking mainnet for realistic testing, or validating DeFi protocols.

  • Write unit tests with Chai assertions and Hardhat fixtures
  • Set up Foundry (Forge) tests in Solidity with fuzzing and cheatcodes
  • Fork mainnet for realistic testing against live contract state
  • Perform gas optimization testing and reporting
  • Impersonate accounts and manipulate blockchain state (time, balances)
  • Generate test coverage reports and verify contracts on Etherscan

How to install web3-testing

npx skills add https://github.com/wshobson/agents --skill web3-testing
Prerequisites
  • Node.js and npm installed
  • Hardhat or Foundry project initialized
  • Environment variables configured (MAINNET_RPC_URL, ETHERSCAN_API_KEY, etc.)
  • Solidity contracts to test
Claude Code
Cursor
Windsurf
Cline

How to use web3-testing

  1. 1.Install dependencies: npm install --save-dev @nomicfoundation/hardhat-toolbox hardhat-gas-reporter solidity-coverage
  2. 2.Configure hardhat.config.js with network settings, gas reporter, and Etherscan API key
  3. 3.Write unit tests in test/ directory using Chai assertions and Hardhat helpers (loadFixture, time, etc.)
  4. 4.For Foundry, write Solidity tests in test/ using forge-std/Test.sol and vm cheatcodes
  5. 5.Run tests with npx hardhat test or forge test
  6. 6.Enable mainnet forking by setting MAINNET_RPC_URL and configuring hardhat.config.js forking block number
  7. 7.Use snapshot/revert pattern (evm_snapshot, evm_revert) for test isolation
  8. 8.Generate gas reports and coverage with npx hardhat test --reporter-options coinmarketcap=<key>

Use cases

Good for
  • Testing ERC20 token transfers and balance changes with event verification
  • Fuzzing smart contract functions with randomized inputs to find edge cases
  • Forking mainnet to test interactions with real Uniswap or other DeFi protocols
  • Validating time-locked operations and state transitions in complex contracts
  • Measuring gas consumption and optimizing contract efficiency
Who it's for
  • Smart contract developers
  • DeFi protocol engineers
  • Blockchain security auditors
  • Web3 QA engineers

web3-testing FAQ

What's the difference between Hardhat and Foundry testing?

Hardhat tests are written in JavaScript using Chai assertions and ethers.js, better for integration testing and complex scenarios. Foundry tests are written in Solidity using vm cheatcodes, faster execution and better for fuzzing and low-level contract testing.

How do I test against live mainnet contracts?

Use mainnet forking by setting the MAINNET_RPC_URL in your config and specifying a blockNumber. Then use ethers.getContractAt() or Foundry's vm.createSelectFork() to interact with real contract addresses.

How can I test with whale accounts or impersonate addresses?

Use hardhat_impersonateAccount RPC method or Foundry's vm.prank() cheatcode to execute transactions as another address, useful for testing with large token balances.

What's the best way to organize test fixtures?

Use Hardhat's loadFixture() helper to create reusable deployment setups. Define an async fixture function that deploys contracts and returns signers/instances, then call it in each test for a clean state.

How do I measure and optimize gas usage?

Enable gasReporter in hardhat.config.js, run tests, and review the gas report. Use Foundry's gas snapshots or check receipt.gasUsed in Hardhat tests to identify expensive operations.

Full instructions (SKILL.md)

Source of truth, from wshobson/agents.


name: web3-testing description: Test smart contracts comprehensively using Hardhat and Foundry with unit tests, integration tests, and mainnet forking. Use when testing Solidity contracts, setting up blockchain test suites, or validating DeFi protocols.

Web3 Smart Contract Testing

Master comprehensive testing strategies for smart contracts using Hardhat, Foundry, and advanced testing patterns.

When to Use This Skill

  • Writing unit tests for smart contracts
  • Setting up integration test suites
  • Performing gas optimization testing
  • Fuzzing for edge cases
  • Forking mainnet for realistic testing
  • Automating test coverage reporting
  • Verifying contracts on Etherscan

Hardhat Testing Setup

// hardhat.config.js
require("@nomicfoundation/hardhat-toolbox");
require("@nomiclabs/hardhat-etherscan");
require("hardhat-gas-reporter");
require("solidity-coverage");

module.exports = {
  solidity: {
    version: "0.8.19",
    settings: {
      optimizer: {
        enabled: true,
        runs: 200,
      },
    },
  },
  networks: {
    hardhat: {
      forking: {
        url: process.env.MAINNET_RPC_URL,
        blockNumber: 15000000,
      },
    },
    goerli: {
      url: process.env.GOERLI_RPC_URL,
      accounts: [process.env.PRIVATE_KEY],
    },
  },
  gasReporter: {
    enabled: true,
    currency: "USD",
    coinmarketcap: process.env.COINMARKETCAP_API_KEY,
  },
  etherscan: {
    apiKey: process.env.ETHERSCAN_API_KEY,
  },
};

Unit Testing Patterns

const { expect } = require("chai");
const { ethers } = require("hardhat");
const {
  loadFixture,
  time,
} = require("@nomicfoundation/hardhat-network-helpers");

describe("Token Contract", function () {
  // Fixture for test setup
  async function deployTokenFixture() {
    const [owner, addr1, addr2] = await ethers.getSigners();

    const Token = await ethers.getContractFactory("Token");
    const token = await Token.deploy();

    return { token, owner, addr1, addr2 };
  }

  describe("Deployment", function () {
    it("Should set the right owner", async function () {
      const { token, owner } = await loadFixture(deployTokenFixture);
      expect(await token.owner()).to.equal(owner.address);
    });

    it("Should assign total supply to owner", async function () {
      const { token, owner } = await loadFixture(deployTokenFixture);
      const ownerBalance = await token.balanceOf(owner.address);
      expect(await token.totalSupply()).to.equal(ownerBalance);
    });
  });

  describe("Transactions", function () {
    it("Should transfer tokens between accounts", async function () {
      const { token, owner, addr1 } = await loadFixture(deployTokenFixture);

      await expect(token.transfer(addr1.address, 50)).to.changeTokenBalances(
        token,
        [owner, addr1],
        [-50, 50],
      );
    });

    it("Should fail if sender doesn't have enough tokens", async function () {
      const { token, addr1 } = await loadFixture(deployTokenFixture);
      const initialBalance = await token.balanceOf(addr1.address);

      await expect(
        token.connect(addr1).transfer(owner.address, 1),
      ).to.be.revertedWith("Insufficient balance");
    });

    it("Should emit Transfer event", async function () {
      const { token, owner, addr1 } = await loadFixture(deployTokenFixture);

      await expect(token.transfer(addr1.address, 50))
        .to.emit(token, "Transfer")
        .withArgs(owner.address, addr1.address, 50);
    });
  });

  describe("Time-based tests", function () {
    it("Should handle time-locked operations", async function () {
      const { token } = await loadFixture(deployTokenFixture);

      // Increase time by 1 day
      await time.increase(86400);

      // Test time-dependent functionality
    });
  });

  describe("Gas optimization", function () {
    it("Should use gas efficiently", async function () {
      const { token } = await loadFixture(deployTokenFixture);

      const tx = await token.transfer(addr1.address, 100);
      const receipt = await tx.wait();

      expect(receipt.gasUsed).to.be.lessThan(50000);
    });
  });
});

Foundry Testing (Forge)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "forge-std/Test.sol";
import "../src/Token.sol";

contract TokenTest is Test {
    Token token;
    address owner = address(1);
    address user1 = address(2);
    address user2 = address(3);

    function setUp() public {
        vm.prank(owner);
        token = new Token();
    }

    function testInitialSupply() public {
        assertEq(token.totalSupply(), 1000000 * 10**18);
    }

    function testTransfer() public {
        vm.prank(owner);
        token.transfer(user1, 100);

        assertEq(token.balanceOf(user1), 100);
        assertEq(token.balanceOf(owner), token.totalSupply() - 100);
    }

    function testFailTransferInsufficientBalance() public {
        vm.prank(user1);
        token.transfer(user2, 100); // Should fail
    }

    function testCannotTransferToZeroAddress() public {
        vm.prank(owner);
        vm.expectRevert("Invalid recipient");
        token.transfer(address(0), 100);
    }

    // Fuzzing test
    function testFuzzTransfer(uint256 amount) public {
        vm.assume(amount > 0 && amount <= token.totalSupply());

        vm.prank(owner);
        token.transfer(user1, amount);

        assertEq(token.balanceOf(user1), amount);
    }

    // Test with cheatcodes
    function testDealAndPrank() public {
        // Give ETH to address
        vm.deal(user1, 10 ether);

        // Impersonate address
        vm.prank(user1);

        // Test functionality
        assertEq(user1.balance, 10 ether);
    }

    // Mainnet fork test
    function testForkMainnet() public {
        vm.createSelectFork("https://eth-mainnet.alchemyapi.io/v2/...");

        // Interact with mainnet contracts
        address dai = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
        assertEq(IERC20(dai).symbol(), "DAI");
    }
}

Advanced Testing Patterns

Snapshot and Revert

describe("Complex State Changes", function () {
  let snapshotId;

  beforeEach(async function () {
    snapshotId = await network.provider.send("evm_snapshot");
  });

  afterEach(async function () {
    await network.provider.send("evm_revert", [snapshotId]);
  });

  it("Test 1", async function () {
    // Make state changes
  });

  it("Test 2", async function () {
    // State reverted, clean slate
  });
});

Mainnet Forking

describe("Mainnet Fork Tests", function () {
  let uniswapRouter, dai, usdc;

  before(async function () {
    await network.provider.request({
      method: "hardhat_reset",
      params: [
        {
          forking: {
            jsonRpcUrl: process.env.MAINNET_RPC_URL,
            blockNumber: 15000000,
          },
        },
      ],
    });

    // Connect to existing mainnet contracts
    uniswapRouter = await ethers.getContractAt(
      "IUniswapV2Router",
      "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D",
    );

    dai = await ethers.getContractAt(
      "IERC20",
      "0x6B175474E89094C44Da98b954EedeAC495271d0F",
    );
  });

  it("Should swap on Uniswap", async function () {
    // Test with real Uniswap contracts
  });
});

Impersonating Accounts

it("Should impersonate whale account", async function () {
  const whaleAddress = "0x...";

  await network.provider.request({
    method: "hardhat_impersonateAccount",
    params: [whaleAddress],
  });

  const whale = await ethers.getSigner(whaleAddress);

  // Use whale's tokens
  await dai
    .connect(whale)
    .transfer(addr1.address, ethers.utils.parseEther("1000"));
});

Additional patterns and templates

More detailed templates and worked examples live in references/details.md. Read that file for the full pattern library.