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.

text
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:

text
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.

text
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.

text
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.

text
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

javascript
// 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

javascript
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

javascript
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

javascript
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

javascript
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

javascript
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

javascript
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.

solidity
// 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

solidity
// 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

solidity
// 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

javascript
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

javascript
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

javascript
// 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

javascript
// 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

javascript
// 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

javascript
// 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

text
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

javascript
// 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

text
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

javascript
// 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

text
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:

text
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

text
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

text
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

text
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