Initial commit
This commit is contained in:
399
skills/web3-testing/SKILL.md
Normal file
399
skills/web3-testing/SKILL.md
Normal file
@@ -0,0 +1,399 @@
|
||||
---
|
||||
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
|
||||
Reference in New Issue
Block a user