400 lines
10 KiB
Markdown
400 lines
10 KiB
Markdown
---
|
|
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
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```javascript
|
|
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)
|
|
|
|
```solidity
|
|
// 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
|
|
```javascript
|
|
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
|
|
```javascript
|
|
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
|
|
```javascript
|
|
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"));
|
|
});
|
|
```
|
|
|
|
## Gas Optimization Testing
|
|
|
|
```javascript
|
|
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);
|
|
});
|
|
});
|
|
```
|
|
|
|
## Coverage Reporting
|
|
|
|
```bash
|
|
# Generate coverage report
|
|
npx hardhat coverage
|
|
|
|
# Output shows:
|
|
# File | % Stmts | % Branch | % Funcs | % Lines |
|
|
# -------------------|---------|----------|---------|---------|
|
|
# contracts/Token.sol | 100 | 90 | 100 | 95 |
|
|
```
|
|
|
|
## Contract Verification
|
|
|
|
```javascript
|
|
// Verify on Etherscan
|
|
await hre.run("verify:verify", {
|
|
address: contractAddress,
|
|
constructorArguments: [arg1, arg2]
|
|
});
|
|
```
|
|
|
|
```bash
|
|
# Or via CLI
|
|
npx hardhat verify --network mainnet CONTRACT_ADDRESS "Constructor arg1" "arg2"
|
|
```
|
|
|
|
## CI/CD Integration
|
|
|
|
```yaml
|
|
# .github/workflows/test.yml
|
|
name: Tests
|
|
|
|
on: [push, pull_request]
|
|
|
|
jobs:
|
|
test:
|
|
runs-on: ubuntu-latest
|
|
|
|
steps:
|
|
- uses: actions/checkout@v2
|
|
- uses: actions/setup-node@v2
|
|
with:
|
|
node-version: '16'
|
|
|
|
- run: npm install
|
|
- run: npx hardhat compile
|
|
- run: npx hardhat test
|
|
- run: npx hardhat coverage
|
|
|
|
- name: Upload coverage to Codecov
|
|
uses: codecov/codecov-action@v2
|
|
```
|
|
|
|
## Resources
|
|
|
|
- **references/hardhat-setup.md**: Hardhat configuration guide
|
|
- **references/foundry-setup.md**: Foundry testing framework
|
|
- **references/test-patterns.md**: Testing best practices
|
|
- **references/mainnet-forking.md**: Fork testing strategies
|
|
- **references/contract-verification.md**: Etherscan verification
|
|
- **assets/hardhat-config.js**: Complete Hardhat configuration
|
|
- **assets/test-suite.js**: Comprehensive test examples
|
|
- **assets/foundry.toml**: Foundry configuration
|
|
- **scripts/test-contract.sh**: Automated testing script
|
|
|
|
## Best Practices
|
|
|
|
1. **Test Coverage**: Aim for >90% coverage
|
|
2. **Edge Cases**: Test boundary conditions
|
|
3. **Gas Limits**: Verify functions don't hit block gas limit
|
|
4. **Reentrancy**: Test for reentrancy vulnerabilities
|
|
5. **Access Control**: Test unauthorized access attempts
|
|
6. **Events**: Verify event emissions
|
|
7. **Fixtures**: Use fixtures to avoid code duplication
|
|
8. **Mainnet Fork**: Test with real contracts
|
|
9. **Fuzzing**: Use property-based testing
|
|
10. **CI/CD**: Automate testing on every commit
|