Test smart contracts comprehensively using Hardhat and Foundry with unit tests, integration tests, and mainnet forking...
This catalog copy contains the inline guidance below, but does not include the following supporting paths. They are external prerequisites, not bundled files:
<support-root>/references/hardhat-setup.md<support-root>/references/foundry-setup.md<support-root>/references/test-patterns.md<support-root>/references/mainnet-forking.md<support-root>/references/contract-verification.md<support-root>/assets/hardhat-config.js<support-root>/assets/test-suite.js<support-root>/assets/foundry.toml<support-root>/scripts/test-contract.shAsk for the original supporting material or an existing installation before using a step that depends on these paths; this catalog does not identify a verified download source for them. Resolve each path relative to that installation, inspect its contents, and verify any applicable license and execution prerequisites. Do not run a command or claim a referenced procedure was followed until its required file exists. Do not fabricate the missing scripts or treat an unrelated file with the same name as equivalent. Independent steps fully documented here can proceed within the userβs authorized scope; report any dependent step as unavailable.
Master comprehensive testing strategies for smart contracts using Hardhat, Foundry, and advanced testing patterns.
The CommonJS snippets illustrate a legacy Hardhat 2 / ethers 5 toolchain. Pin compatible reviewed versions from the project lockfile; do not combine them with Hardhat 3 / ethers 6 without migration. Contract fixtures and interfaces are project-specific prerequisites, not bundled deployable code. Use local development chains and disposable keys only. Forked state is local; never broadcast or deploy on a funded network as a testing shortcut. Remote source verification is a separate publication action.
// 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,
},
},
},
gasReporter: {
enabled: true,
currency: "USD",
coinmarketcap: process.env.COINMARKETCAP_API_KEY,
},
etherscan: {
apiKey: process.env.ETHERSCAN_API_KEY,
},
};
const { expect } = require("chai");
const { ethers, network } = 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, owner, 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, addr1 } = await loadFixture(deployTokenFixture);
const tx = await token.transfer(addr1.address, 100);
const receipt = await tx.wait();
expect(receipt.gasUsed).to.be.lessThan(50000);
});
});
});
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
import "../src/Token.sol";
interface IERC20MetadataForTest {
function symbol() external view returns (string memory);
}
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 testTransferInsufficientBalance() public {
vm.expectRevert(); // Prefer the exact expected error from the tested contract.
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(vm.envString("MAINNET_RPC_URL"), 15000000);
// Interact with mainnet contracts
address dai = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
assertEq(IERC20MetadataForTest(dai).symbol(), "DAI");
}
}
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
});
});
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
});
});
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"));
});
const { expect } = require("chai");
describe("Gas Optimization", function () {
it("Compare gas usage between implementations", async function () {
const Implementation1 =
await ethers.getContractFactory("OptimizedContract");
const Implementation2 = await ethers.getContractFactory(
"UnoptimizedContract",
);
const contract1 = await Implementation1.deploy();
const contract2 = await Implementation2.deploy();
const tx1 = await contract1.doSomething();
const receipt1 = await tx1.wait();
const tx2 = await contract2.doSomething();
const receipt2 = await tx2.wait();
console.log("Optimized gas:", receipt1.gasUsed.toString());
console.log("Unoptimized gas:", receipt2.gasUsed.toString());
expect(receipt1.gasUsed).to.be.lessThan(receipt2.gasUsed);
});
});
# Generate coverage report
npx hardhat coverage
# Output shows:
# File | % Stmts | % Branch | % Funcs | % Lines |
# -------------------|---------|----------|---------|---------|
# contracts/Token.sol | 100 | 90 | 100 | 95 |
// Verify on Etherscan
await hre.run("verify:verify", {
address: contractAddress,
constructorArguments: [arg1, arg2],
});
# Or via CLI
npx hardhat verify --network mainnet CONTRACT_ADDRESS "Constructor arg1" "arg2"
Use the repository's supported Node version and reviewed, commit-pinned Actions. Install from a committed lockfile, compile, run local-chain tests and collect coverage. Keep RPC credentials in the platform secret store; do not expose secrets to untrusted fork jobs. External coverage upload is optional and needs authorization for any source or coverage data disclosed. A green coverage percentage alone is not a security audit.