Skills · Coding

Web3 Smart Contract Testing

Unverified24/40

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.

Originally by wshobson · MIT

Claude CodePartialHas SKILL.md but declares no allowed-tools — Claude Code will ask for permission each time
CursorPartialPlain prose you can paste in — but no Cursor rules file
CodexPartialPlain prose you can paste in — but no AGENTS.md
Gemini CLIPartialPlain prose you can paste in
CopilotPartialPlain prose you can paste in — but no Copilot instructions file
npx agentalley add web3-testing

This command does not work yet — the CLI is still being built. Until then, use Raw in the reader below to take the file.

Who is stuck, and on what

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.

The whole source

No sign-in, no blur, nothing truncated
web3-testing/SKILL.md312 lines7.7 KBRawView on GitHub
Frontmatter — 2 properties
nameweb3-testing
descriptionTest 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.
1---
2name: web3-testing
3description: 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.
4---A5No allowed-tools declared — no way to tell what this skill may touch
5 
6# Web3 Smart Contract Testing
7 
8Master comprehensive testing strategies for smart contracts using Hardhat, Foundry, and advanced testing patterns.
9 
10## When to Use This Skill
11 
12- Writing unit tests for smart contracts
13- Setting up integration test suites
14- Performing gas optimization testing
15- Fuzzing for edge cases
16- Forking mainnet for realistic testing
17- Automating test coverage reporting
18- Verifying contracts on Etherscan
19 
20## Hardhat Testing Setup
21 
22```javascript
23// hardhat.config.js
24require("@nomicfoundation/hardhat-toolbox");
25require("@nomiclabs/hardhat-etherscan");
26require("hardhat-gas-reporter");
27require("solidity-coverage");
28 
29module.exports = {
30 solidity: {
31 version: "0.8.19",
32 settings: {
33 optimizer: {
34 enabled: true,
35 runs: 200,
36 },
37 },
38 },
39 networks: {
40 hardhat: {
41 forking: {
42 url: process.env.MAINNET_RPC_URL,
43 blockNumber: 15000000,
44 },
45 },
46 goerli: {
47 url: process.env.GOERLI_RPC_URL,
48 accounts: [process.env.PRIVATE_KEY],
49 },
50 },
51 gasReporter: {
52 enabled: true,
53 currency: "USD",
54 coinmarketcap: process.env.COINMARKETCAP_API_KEY,
55 },
56 etherscan: {
57 apiKey: process.env.ETHERSCAN_API_KEY,
58 },
59};
60```
61 
62## Unit Testing Patterns
63 
64```javascript
65const { expect } = require("chai");
66const { ethers } = require("hardhat");
67const {
68 loadFixture,
69 time,
70} = require("@nomicfoundation/hardhat-network-helpers");
71 
72describe("Token Contract", function () {
73 // Fixture for test setup
74 async function deployTokenFixture() {
75 const [owner, addr1, addr2] = await ethers.getSigners();
76 
77 const Token = await ethers.getContractFactory("Token");
78 const token = await Token.deploy();
79 
80 return { token, owner, addr1, addr2 };
81 }
82 
83 describe("Deployment", function () {
84 it("Should set the right owner", async function () {
85 const { token, owner } = await loadFixture(deployTokenFixture);
86 expect(await token.owner()).to.equal(owner.address);
87 });
88 
89 it("Should assign total supply to owner", async function () {
90 const { token, owner } = await loadFixture(deployTokenFixture);
91 const ownerBalance = await token.balanceOf(owner.address);
92 expect(await token.totalSupply()).to.equal(ownerBalance);
93 });
94 });
95 
96 describe("Transactions", function () {
97 it("Should transfer tokens between accounts", async function () {
98 const { token, owner, addr1 } = await loadFixture(deployTokenFixture);
99 
100 await expect(token.transfer(addr1.address, 50)).to.changeTokenBalances(
101 token,
102 [owner, addr1],
103 [-50, 50],
104 );
105 });
106 
107 it("Should fail if sender doesn't have enough tokens", async function () {
108 const { token, addr1 } = await loadFixture(deployTokenFixture);
109 const initialBalance = await token.balanceOf(addr1.address);
110 
111 await expect(
112 token.connect(addr1).transfer(owner.address, 1),
113 ).to.be.revertedWith("Insufficient balance");
114 });
115 
116 it("Should emit Transfer event", async function () {
117 const { token, owner, addr1 } = await loadFixture(deployTokenFixture);
118 
119 await expect(token.transfer(addr1.address, 50))
120 .to.emit(token, "Transfer")
121 .withArgs(owner.address, addr1.address, 50);
122 });
123 });
124 
125 describe("Time-based tests", function () {
126 it("Should handle time-locked operations", async function () {
127 const { token } = await loadFixture(deployTokenFixture);
128 
129 // Increase time by 1 day
130 await time.increase(86400);
131 
132 // Test time-dependent functionality
133 });
134 });
135 
136 describe("Gas optimization", function () {
137 it("Should use gas efficiently", async function () {
138 const { token } = await loadFixture(deployTokenFixture);
139 
140 const tx = await token.transfer(addr1.address, 100);
141 const receipt = await tx.wait();
142 
143 expect(receipt.gasUsed).to.be.lessThan(50000);
144 });
145 });
146});
147```
148 
149## Foundry Testing (Forge)
150 
151```solidity
152// SPDX-License-Identifier: MIT
153pragma solidity ^0.8.0;
154 
155import "forge-std/Test.sol";
156import "../src/Token.sol";
157 
158contract TokenTest is Test {
159 Token token;
160 address owner = address(1);
161 address user1 = address(2);
162 address user2 = address(3);
163 
164 function setUp() public {
165 vm.prank(owner);
166 token = new Token();
167 }
168 
169 function testInitialSupply() public {
170 assertEq(token.totalSupply(), 1000000 * 10**18);
171 }
172 
173 function testTransfer() public {
174 vm.prank(owner);
175 token.transfer(user1, 100);
176 
177 assertEq(token.balanceOf(user1), 100);
178 assertEq(token.balanceOf(owner), token.totalSupply() - 100);
179 }
180 
181 function testFailTransferInsufficientBalance() public {
182 vm.prank(user1);
183 token.transfer(user2, 100); // Should fail
184 }
185 
186 function testCannotTransferToZeroAddress() public {
187 vm.prank(owner);
188 vm.expectRevert("Invalid recipient");
189 token.transfer(address(0), 100);
190 }
191 
192 // Fuzzing test
193 function testFuzzTransfer(uint256 amount) public {
194 vm.assume(amount > 0 && amount <= token.totalSupply());
195 
196 vm.prank(owner);
197 token.transfer(user1, amount);
198 
199 assertEq(token.balanceOf(user1), amount);
200 }
201 
202 // Test with cheatcodes
203 function testDealAndPrank() public {
204 // Give ETH to address
205 vm.deal(user1, 10 ether);
206 
207 // Impersonate address
208 vm.prank(user1);
209 
210 // Test functionality
211 assertEq(user1.balance, 10 ether);
212 }
213 
214 // Mainnet fork test
215 function testForkMainnet() public {
216 vm.createSelectFork("https://eth-mainnet.alchemyapi.io/v2/...");A2Sends to eth-mainnet.alchemyapi.io — outside the allowlist, and this file also reads environment variables or keys
217 
218 // Interact with mainnet contracts
219 address dai = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
220 assertEq(IERC20(dai).symbol(), "DAI");
221 }
222}
223```
224 
225## Advanced Testing Patterns
226 
227### Snapshot and Revert
228 
229```javascript
230describe("Complex State Changes", function () {
231 let snapshotId;
232 
233 beforeEach(async function () {
234 snapshotId = await network.provider.send("evm_snapshot");
235 });
236 
237 afterEach(async function () {
238 await network.provider.send("evm_revert", [snapshotId]);
239 });
240 
241 it("Test 1", async function () {
242 // Make state changes
243 });
244 
245 it("Test 2", async function () {
246 // State reverted, clean slate
247 });
248});
249```
250 
251### Mainnet Forking
252 
253```javascript
254describe("Mainnet Fork Tests", function () {
255 let uniswapRouter, dai, usdc;
256 
257 before(async function () {
258 await network.provider.request({
259 method: "hardhat_reset",
260 params: [
261 {
262 forking: {
263 jsonRpcUrl: process.env.MAINNET_RPC_URL,
264 blockNumber: 15000000,
265 },
266 },
267 ],
268 });
269 
270 // Connect to existing mainnet contracts
271 uniswapRouter = await ethers.getContractAt(
272 "IUniswapV2Router",
273 "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D",
274 );
275 
276 dai = await ethers.getContractAt(
277 "IERC20",
278 "0x6B175474E89094C44Da98b954EedeAC495271d0F",
279 );
280 });
281 
282 it("Should swap on Uniswap", async function () {
283 // Test with real Uniswap contracts
284 });
285});
286```
287 
288### Impersonating Accounts
289 
290```javascript
291it("Should impersonate whale account", async function () {
292 const whaleAddress = "0x...";
293 
294 await network.provider.request({
295 method: "hardhat_impersonateAccount",
296 params: [whaleAddress],
297 });
298 
299 const whale = await ethers.getSigner(whaleAddress);
300 
301 // Use whale's tokens
302 await dai
303 .connect(whale)
304 .transfer(addr1.address, ethers.utils.parseEther("1000"));
305});
306```
307 
308## Additional patterns and templates
309 
310More detailed templates and worked examples live in `references/details.md`. Read that file for the full pattern library.
311 
312 

Reviews

Installed this one?Write the first review and take the Trailblazer badge.

Reviews only open after a real install, so this is empty — and we leave it empty rather than invent one.

Alternatives

Also in Coding