Learning Objectives:
-
Master comprehensive testing strategies for smart contracts
-
Understand deployment pipelines and mainnet considerations
-
Learn contract verification and security practices
-
Analyze upgradeable contracts and maintenance strategies
4.7.1: The Importance of Testing Smart Contracts
Why Testing is Critical:
Smart contracts handle real value, are immutable after deployment, and bugs can lead to catastrophic losses. Testing is not optional—it is essential for security and reliability.
The Cost of Not Testing: 1. Financial Risk: - Direct loss of user funds - Protocol collapse - Reputation damage 2. Immutability: - Once deployed, cannot fix easily - Bugs are permanent - Only upgrades can fix (if designed) 3. Complexity: - Smart contracts interact in complex ways - Edge cases are common - Attack vectors are numerous 4. Trust: - Users trust audited code - Bugs erode trust - Recovery is difficult Testing Statistics: - 70% of vulnerabilities found in testing - 20% found in audits - 10% found in production (too late)
The Testing Pyramid for Smart Contracts:
Testing Pyramid: ┌─────────────────────────────────────────────────────────────────────┐ │ Testing Pyramid │ │ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ Manual / Exploratory Testing │ │ │ │ (Human review, user testing) │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ Integration Tests │ │ │ │ (Contract interactions, multiple contracts) │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ Unit Tests │ │ │ │ (Individual functions, edge cases) │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ Property-Based / Fuzz Testing │ │ │ │ (Random inputs, invariant checking) │ │ │ └─────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────┘ Goal: Catch vulnerabilities at every level Target: 95%+ test coverage
4.7.2: Testing Frameworks – Comprehensive Comparison
Hardhat – Most Popular Framework
Hardhat is a development environment for Ethereum that provides testing, deployment, debugging, and more.
Hardhat Overview: Features: - Built-in Hardhat Network (local EVM) - Console.log debugging - Solidity stack traces - TypeScript support - Plugin system - Task runner Installation: npm install --save-dev hardhat npx hardhat Key Components: 1. Hardhat Network: Local blockchain 2. Hardhat Runtime: Environment 3. Hardhat Tasks: Custom scripts 4. Hardhat Plugins: Extensibility
Foundry – Fast Testing Framework
Foundry is a Rust-based toolkit for Ethereum development known for its speed and advanced testing features.
Foundry Overview: Features: - Very fast (written in Rust) - Fuzzing (property-based testing) - Invariant testing - Solidity scripting - Gas tracking - Differential testing Installation: foundryup Key Components: 1. Forge: Testing framework 2. Cast: CLI tool for EVM 3. Anvil: Local node 4. Chisel: Solidity REPL Advantages: - 10-100x faster than Hardhat - Built-in fuzzing - No JavaScript required - More deterministic
Truffle – Mature Testing Suite
Truffle is a comprehensive development suite with a long history in the Ethereum ecosystem.
Truffle Overview: Features: - Migration framework - Testing with Mocha/Chai - Interactive console - Network management - Ganache (local blockchain) Installation: npm install -g truffle Key Components: 1. Truffle Suite: Complete toolkit 2. Ganache: Local blockchain 3. Drizzle: Frontend library 4. Migrations: Deployment scripts
Framework Comparison:
| Feature | Hardhat | Foundry | Truffle |
|---|---|---|---|
| Language | JS/TS | Solidity/Rust | JS |
| Testing Speed | Medium | Very Fast | Slow |
| Fuzzing | Limited (plugin) | Built-in | Limited |
| Debugging | Excellent | Good | Good |
| TypeScript | Built-in | Limited | Plugin |
| Gas Tracking | Good | Excellent | Good |
| Learning Curve | Medium | Medium | High |
| Ecosystem | Very Large | Growing | Large |
| Best For | General | Advanced | Beginners |
4.7.3: Writing Unit Tests in Hardhat
Basic Test Structure
// test/MyContract.test.js const { expect } = require('chai'); const { ethers } = require('hardhat'); describe('MyContract', function () { let MyContract; let myContract; let owner; let addr1; let addr2; // Runs before each test beforeEach(async function () { [owner, addr1, addr2] = await ethers.getSigners(); MyContract = await ethers.getContractFactory('MyContract'); myContract = await MyContract.deploy(); await myContract.deployed(); }); describe('Deployment', function () { it('Should set the right owner', async function () { expect(await myContract.owner()).to.equal(owner.address); }); it('Should have correct initial state', async function () { expect(await myContract.value()).to.equal(0); }); }); describe('Functions', function () { it('Should allow owner to set value', async function () { await myContract.setValue(42); expect(await myContract.value()).to.equal(42); }); it('Should not allow non-owner to set value', async function () { await expect( myContract.connect(addr1).setValue(42) ).to.be.revertedWith('Not owner'); }); it('Should emit correct event', async function () { await expect(myContract.setValue(42)) .to.emit(myContract, 'ValueChanged') .withArgs(42); }); }); });
Testing Edge Cases
describe('Edge Cases', function () { it('Should handle zero value', async function () { await myContract.setValue(0); expect(await myContract.value()).to.equal(0); }); it('Should handle maximum value', async function () { const maxValue = ethers.MaxUint256; await myContract.setValue(maxValue); expect(await myContract.value()).to.equal(maxValue); }); it('Should revert on overflow', async function () { const maxValue = ethers.MaxUint256; await myContract.setValue(maxValue); await expect(myContract.increment()).to.be.reverted; }); });
Testing with Modifiers
describe('Modifiers', function () { it('Should enforce onlyOwner', async function () { // Owner can call await myContract.ownerFunction(); // Non-owner cannot await expect( myContract.connect(addr1).ownerFunction() ).to.be.revertedWith('Not owner'); }); it('Should enforce whenNotPaused', async function () { // When not paused, works await myContract.normalFunction(); // When paused, reverts await myContract.pause(); await expect( myContract.normalFunction() ).to.be.revertedWith('Paused'); }); });
Testing Events
describe('Events', function () { it('Should emit event on transfer', async function () { const amount = ethers.parseEther('1.0'); await expect(myContract.transfer(addr1.address, amount)) .to.emit(myContract, 'Transfer') .withArgs(owner.address, addr1.address, amount); }); it('Should emit multiple events', async function () { const tx = await myContract.complexFunction(); const receipt = await tx.wait(); // Check events in order expect(receipt.events[0].event).to.equal('Event1'); expect(receipt.events[1].event).to.equal('Event2'); }); it('Should emit events with indexed fields', async function () { const filter = myContract.filters.Transfer(owner.address, null); const events = await myContract.queryFilter(filter); expect(events.length).to.be.greaterThan(0); }); });
4.7.4: Advanced Testing with Hardhat
Mocking and Impersonation
describe('Impersonation', function () { it('Should impersonate any address', async function () { // Impersonate a specific address await hre.network.provider.request({ method: 'hardhat_impersonateAccount', params: ['0x123...'] }); const impersonatedSigner = await ethers.getSigner('0x123...'); // Now you can act as that address await myContract.connect(impersonatedSigner).function(); }); it('Should mine blocks', async function () { // Mine specific number of blocks await hre.network.provider.request({ method: 'evm_mine', params: [10] }); }); });
Time Manipulation
describe('Time Manipulation', function () { it('Should handle time-based functions', async function () { // Get current block time const block = await ethers.provider.getBlock('latest'); const currentTime = block.timestamp; // Move time forward await hre.network.provider.request({ method: 'evm_increaseTime', params: [3600 * 24] // 1 day }); // Mine a block to apply time change await hre.network.provider.request({ method: 'evm_mine' }); // Now time has advanced const newBlock = await ethers.provider.getBlock('latest'); expect(newBlock.timestamp).to.be.greaterThan(currentTime); }); });
Snapshot and Revert
describe('Snapshot Testing', function () { it('Should take snapshots', async function () { // Take snapshot const snapshot = await hre.network.provider.request({ method: 'evm_snapshot' }); // Do operations await myContract.setValue(100); expect(await myContract.value()).to.equal(100); // Revert to snapshot await hre.network.provider.request({ method: 'evm_revert', params: [snapshot] }); // State restored expect(await myContract.value()).to.equal(0); }); });
4.7.5: Fuzzing and Property-Based Testing with Foundry
Fuzzing Tests
Fuzzing tests run many random inputs to find edge cases and vulnerabilities.
// test/MyContract.t.sol // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "forge-std/Test.sol"; import "../src/MyContract.sol"; contract MyContractTest is Test { MyContract public myContract; address public owner; address public addr1; function setUp() public { owner = address(this); addr1 = makeAddr("addr1"); myContract = new MyContract(); } // Fuzz test: test with random values function testFuzzSetValue(uint256 value) public { myContract.setValue(value); assertEq(myContract.value(), value); } // Fuzz test with constraints function testFuzzTransfer(uint256 amount) public { // Constrain amount to avoid overflow amount = bound(amount, 1, 1000); // Only owner can transfer vm.prank(owner); myContract.transfer(addr1, amount); // Check balances assertEq(myContract.balanceOf(addr1), amount); } // Invariant test function testInvariantTotalSupply() public { // Total supply should always equal sum of balances // This runs many times with random actions } }
Assertions in Foundry
// Basic assertions assertEq(a, b); // a == b assertTrue(condition); // condition is true assertFalse(condition); // condition is false // Revert assertions vm.expectRevert("Not owner"); myContract.setValue(100); // Event assertions vm.expectEmit(true, true, false, true); emit Transfer(owner, addr1, 100); myContract.transfer(addr1, 100);
Foundry Cheatcodes
// Impersonation vm.prank(address); // Call as address vm.startPrank(address); // Multiple calls as address vm.stopPrank(); // Stop impersonation // Time manipulation vm.warp(timestamp); // Set block.timestamp vm.roll(blockNumber); // Set block.number vm.skip(seconds); // Skip time // Storage manipulation vm.store(contract, slot, value); // Write to storage bytes32 value = vm.load(contract, slot); // Read storage // Snapshots uint256 snapshot = vm.snapshot(); // Do operations vm.revertTo(snapshot); // Labels vm.label(address, "label"); // Label addresses in traces // Assume vm.assume(condition); // Skip test if condition false
4.7.6: Integration Testing
Testing Multiple Contracts
describe('Integration Tests', function () { let token; let vault; let owner; let user; beforeEach(async function () { [owner, user] = await ethers.getSigners(); // Deploy token const Token = await ethers.getContractFactory('ERC20'); token = await Token.deploy('Test Token', 'TST', 18, 1000000); await token.deployed(); // Deploy vault const Vault = await ethers.getContractFactory('Vault'); vault = await Vault.deploy(token.address); await vault.deployed(); // Mint tokens to user await token.transfer(user.address, 1000); }); it('Should deposit tokens into vault', async function () { const depositAmount = 100; // Approve vault to spend tokens await token.connect(user).approve(vault.address, depositAmount); // Deposit into vault await vault.connect(user).deposit(depositAmount); // Check vault balance expect(await token.balanceOf(vault.address)).to.equal(depositAmount); expect(await vault.balanceOf(user.address)).to.equal(depositAmount); }); it('Should calculate yield correctly', async function () { await vault.connect(user).deposit(100); // Simulate yield generation await vault.addYield(10); // Check shares value increased const shares = await vault.balanceOf(user.address); const assets = await vault.convertToAssets(shares); expect(assets).to.be.greaterThan(shares); }); });
Testing External Dependencies
describe('External Dependencies', function () { // Mock contracts for testing // Mock Chainlink Oracle contract MockOracle { int256 public price; function setPrice(int256 _price) public { price = _price; } function latestRoundData() public view returns (uint80, int256, uint256, uint256, uint80) { return (0, price, 0, 0, 0); } } // Use mocks in tests it('Should use mocked oracle', async function () { // Deploy mock const MockOracle = await ethers.getContractFactory('MockOracle'); const oracle = await MockOracle.deploy(); await oracle.deployed(); // Set price await oracle.setPrice(1000); // Deploy contract with mock const PriceConsumer = await ethers.getContractFactory('PriceConsumer'); const consumer = await PriceConsumer.deploy(oracle.address); await consumer.deployed(); // Test expect(await consumer.getPrice()).to.equal(1000); }); });
4.7.7: Deployment Strategies
Basic Deployment
// scripts/deploy.js const hre = require('hardhat'); async function main() { // Get deployer const [deployer] = await hre.ethers.getSigners(); console.log('Deploying with:', deployer.address); // Get balance const balance = await ethers.provider.getBalance(deployer.address); console.log('Balance:', ethers.formatEther(balance)); // Deploy contract const MyContract = await hre.ethers.getContractFactory('MyContract'); const myContract = await MyContract.deploy(); await myContract.deployed(); console.log('MyContract deployed to:', myContract.address); // Save deployment info const deployment = { address: myContract.address, abi: MyContract.interface.format('json'), network: hre.network.name }; // Write to file const fs = require('fs'); fs.writeFileSync( './deployments/' + hre.network.name + '.json', JSON.stringify(deployment, null, 2) ); } main() .then(() => process.exit(0)) .catch((error) => { console.error(error); process.exit(1); });
Deploying with Arguments
// scripts/deploy-with-args.js async function main() { const MyContract = await ethers.getContractFactory('MyContract'); // Constructor arguments const name = "My Token"; const symbol = "MTK"; const initialSupply = ethers.parseEther('1000000'); const myContract = await MyContract.deploy(name, symbol, initialSupply); await myContract.deployed(); console.log('MyContract deployed to:', myContract.address); }
Deploying to Different Networks
// hardhat.config.js module.exports = { solidity: '0.8.20', networks: { localhost: { url: 'http://127.0.0.1:8545' }, goerli: { url: process.env.GOERLI_RPC_URL, accounts: [process.env.PRIVATE_KEY] }, sepolia: { url: process.env.SEPOLIA_RPC_URL, accounts: [process.env.PRIVATE_KEY] }, mainnet: { url: process.env.MAINNET_RPC_URL, accounts: [process.env.PRIVATE_KEY] } }, etherscan: { apiKey: process.env.ETHERSCAN_API_KEY } }; // Deploy to specific network // npx hardhat run scripts/deploy.js --network goerli
Multi-Sig Deployment
// scripts/deploy-multisig.js async function main() { const [deployer] = await ethers.getSigners(); // Deploy implementation const Implementation = await ethers.getContractFactory('Implementation'); const implementation = await Implementation.deploy(); await implementation.deployed(); console.log('Implementation:', implementation.address); // Deploy proxy const Proxy = await ethers.getContractFactory('Proxy'); const proxy = await Proxy.deploy(implementation.address); await proxy.deployed(); console.log('Proxy:', proxy.address); // Transfer ownership to multi-sig const multisig = '0x...'; const proxyAdmin = await ethers.getContractAt('ProxyAdmin', proxy.address); await proxyAdmin.transferOwnership(multisig); console.log('Ownership transferred to multi-sig:', multisig); }
4.7.8: Contract Verification
Verification on Etherscan
Verification Process: 1. Hardhat Verification: npx hardhat verify --network goerli 0x123... "arg1" "arg2" 2. With Constructor Arguments: npx hardhat verify --network goerli 0x123... "arg1" "arg2" 3. Using Etherscan API: npx hardhat verify --network mainnet --contract contracts/MyContract.sol:MyContract 0x123... 4. Flattened Code Verification: - Flatten contract: npx hardhat flatten contracts/MyContract.sol > MyContract_flat.sol - Upload to Etherscan manually
Automated Verification
// scripts/verify.js const hre = require('hardhat'); async function main() { const contractAddress = '0x123...'; const constructorArgs = []; try { await hre.run('verify:verify', { address: contractAddress, constructorArguments: constructorArgs }); console.log('Verification successful'); } catch (error) { if (error.message.includes('Already Verified')) { console.log('Contract already verified'); } else { console.error('Verification failed:', error); } } } main() .then(() => process.exit(0)) .catch((error) => { console.error(error); process.exit(1); });
4.7.9: Upgradeable Contracts
Proxy Pattern Types
Proxy Patterns: 1. Transparent Proxy (OpenZeppelin): - Admin and user separation - Admin calls upgrade - Users call logic 2. UUPS Proxy (Universal Upgradeable Proxy Standard): - Upgrade logic in implementation - Smaller proxy - More gas efficient 3. Beacon Proxy: - Multiple proxies share implementation - Single point of upgrade - Used for clones 4. Diamond Proxy (EIP-2535): - Multiple implementations - Modular upgrades - Complex but flexible
OpenZeppelin Upgrades
// scripts/upgrade.js const { upgrades } = require('hardhat'); async function main() { // Deploy upgradeable contract const MyContract = await ethers.getContractFactory('MyContract'); const myContract = await upgrades.deployProxy(MyContract, ['arg1']); await myContract.deployed(); console.log('Deployed to:', myContract.address); // Upgrade contract const MyContractV2 = await ethers.getContractFactory('MyContractV2'); const upgraded = await upgrades.upgradeProxy(myContract.address, MyContractV2); console.log('Upgraded to:', upgraded.address); } // Requirements for upgrade: // - Same storage layout // - No breaking changes // - Contract is prepared
4.7.10: Production Deployment Checklist
Pre-Deployment Checklist: ☐ All tests passing ☐ Test coverage > 95% ☐ Multiple audits completed ☐ Gas optimization done ☐ Contract verified on testnet ☐ Documentation complete ☐ Upgrade path defined (if needed) ☐ Emergency procedures documented ☐ Multi-sig configured ☐ Timelocks implemented ☐ Monitoring configured ☐ Alerts set up ☐ Insurance considered ☐ Bug bounty ready ☐ Deployment script tested ☐ Verification script ready ☐ Rollback plan prepared ☐ Team has access ☐ Communication plan ready ☐ Launch timeline defined ☐ Post-launch plan ready
Post-Deployment Checklist:
Post-Deployment Actions: ☐ Verify contract on Etherscan ☐ Set up monitoring ☐ Transfer ownership to multi-sig ☐ Add liquidity (if token) ☐ Announce deployment ☐ Monitor transactions ☐ Check gas costs ☐ Watch for issues ☐ Respond to user feedback ☐ Regular backups ☐ Performance monitoring ☐ Security monitoring ☐ Regular updates
1. Gas Optimization Testing
Gas Testing:
// Hardhat gas reporting
const tx = await contract.function();
const receipt = await tx.wait();
console.log('Gas used:', receipt.gasUsed.toString());
// Foundry gas tracking
function testGas() public {
uint256 gasStart = gasleft();
myContract.function();
uint256 gasUsed = gasStart - gasleft();
console.log("Gas used:", gasUsed);
}
// Compare gas costs
// Use: npx hardhat test --gas
2. Mainnet Fork Testing
Fork Testing:
// hardhat.config.js
networks: {
mainnetFork: {
url: `https://mainnet.infura.io/v3/${INFURA_KEY}`,
blockNumber: 17000000
}
}
// Test with fork
// npx hardhat test --network mainnetFork
// In tests
// You can interact with real mainnet contracts
const uniswap = await ethers.getContractAt('IUniswap', '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D');
3. Deployment Errors
Common Deployment Errors: 1. Insufficient Funds: Error: insufficient funds for gas Solution: Fund deployer address 2. Network Issues: Error: network does not support EIP-1559 Solution: Use legacy transactions 3. Contract Already Deployed: Error: contract already exists Solution: Use different address or salt 4. Verification Errors: Error: constructor arguments mismatch Solution: Verify arguments match deployment 5. Upgrade Errors: Error: storage layout mismatch Solution: Maintain storage order