# Smart Contract Testing Strategy ## Testing Pyramid ``` ┌─────────────┐ │ Fork Test │ ← Tests against real on-chain state ┌┴─────────────┴┐ │ Invariant Test │ ← System invariant verification ┌┴───────────────┴┐ │ Fuzz Testing │ ← Random input testing ┌┴─────────────────┴┐ │ Unit Testing │ ← Basic functionality verification └───────────────────┘ ``` --- ## 1. Unit Testing (Foundry) ```solidity contract VaultTest is Test { Vault vault; ERC20Mock token; address alice = makeAddr("alice"); function setUp() public { token = new ERC20Mock(); vault = new Vault(address(token)); token.mint(alice, 1000e18); vm.prank(alice); token.approve(address(vault), type(uint256).max); } function test_deposit() public { vm.prank(alice); vault.deposit(100e18); assertEq(vault.balanceOf(alice), 100e18); } // Test revert function test_RevertWhen_DepositZero() public { vm.prank(alice); vm.expectRevert(Vault.ZeroAmount.selector); vault.deposit(0); } // Test event function test_EmitDeposit() public { vm.expectEmit(true, false, false, true); emit Deposit(alice, 100e18); // Expected event vm.prank(alice); vault.deposit(100e18); } } ``` --- ## 2. Fuzz Testing ```solidity // Foundry auto-generates random inputs (256 runs by default) function testFuzz_deposit(uint256 amount) public { // Constrain input to valid range amount = bound(amount, 1, 1000e18); token.mint(alice, amount); vm.prank(alice); vault.deposit(amount); assertEq(vault.balanceOf(alice), amount); assertEq(token.balanceOf(address(vault)), amount); } // Multi-parameter fuzz function testFuzz_transferAndWithdraw(uint256 depositAmt, uint256 withdrawAmt) public { depositAmt = bound(depositAmt, 1e18, 1000e18); withdrawAmt = bound(withdrawAmt, 1, depositAmt); // Cannot exceed deposit amount vm.startPrank(alice); vault.deposit(depositAmt); vault.withdraw(withdrawAmt); vm.stopPrank(); assertEq(vault.balanceOf(alice), depositAmt - withdrawAmt); } ``` Increase fuzz run count: ```toml # foundry.toml [fuzz] runs = 10000 # Default is 256; 10000+ recommended for high-risk contracts ``` --- ## 3. Invariant Testing (Most Powerful) **Invariants**: Conditions the system must always satisfy regardless of what operations occur. ```solidity // Handler: defines the set of valid operations contract VaultHandler is CommonBase, StdCheats, StdUtils { Vault vault; ERC20Mock token; uint256 public totalDeposited; constructor(Vault _vault, ERC20Mock _token) { vault = _vault; token = _token; } function deposit(uint256 amount) public { amount = bound(amount, 1, 1e30); token.mint(msg.sender, amount); vm.prank(msg.sender); token.approve(address(vault), amount); vm.prank(msg.sender); vault.deposit(amount); totalDeposited += amount; } function withdraw(uint256 amount) public { amount = bound(amount, 0, vault.balanceOf(msg.sender)); if (amount == 0) return; vm.prank(msg.sender); vault.withdraw(amount); totalDeposited -= amount; } } // Invariant test contract contract VaultInvariantTest is Test { Vault vault; VaultHandler handler; function setUp() public { vault = new Vault(...); handler = new VaultHandler(vault, token); targetContract(address(handler)); // Foundry randomly calls handler functions } // Invariant 1: vault token balance = sum of all deposits function invariant_vaultBalanceMatchesDeposits() public { assertEq(token.balanceOf(address(vault)), handler.totalDeposited()); } // Invariant 2: cannot withdraw more than deposited function invariant_noFreeWithdrawal() public { assertGe(handler.totalDeposited(), 0); } } ``` --- ## 4. Fork Testing (Mainnet State) ```solidity contract ForkTest is Test { address constant UNISWAP_V3_POOL = 0x...; address constant WHALE = 0xWHALE_ADDRESS; function setUp() public { vm.createSelectFork(vm.envString("MAINNET_RPC"), 19000000); // Can now interact directly with real contracts } function test_swapOnRealUniswap() public { // Impersonate a whale vm.prank(WHALE); // Interact with real Uniswap V3 ISwapRouter(UNISWAP_ROUTER).exactInputSingle(...); } function test_aaveLiquidation() public { // Simulate price crash → trigger liquidation vm.mockCall(CHAINLINK_ORACLE, abi.encodeWithSelector(latestRoundData.selector), abi.encode(0, 1000e8, 0, block.timestamp, 0)); // ETH price drops to $1000 // Execute liquidation aavePool.liquidationCall(...); } } ``` --- ## Coverage Targets | Contract Type | Target Coverage | |---------|----------| | Core DeFi Logic | **>95%** | | Access Control / Permissions | **100%** | | Utility / Helper Functions | >80% | | View Functions | >70% | ```bash forge coverage --report lcov genhtml lcov.info -o coverage-report ``` ## Pre-Audit Checklist - [ ] All public/external functions have unit tests - [ ] Fuzz tests cover all numeric inputs - [ ] Critical invariants have Invariant tests - [ ] Fork tests verify integration with mainnet protocols - [ ] Gas snapshots compared between versions # Smart Contract Testing Strategy ## Testing Pyramid ``` ┌─────────────┐ │ Fork Test │ ← Tests against real on-chain state ┌┴─────────────┴┐ │ Invariant Test │ ← System invariant verification ┌┴───────────────┴┐ │ Fuzz Testing │ ← Random input testing ┌┴─────────────────┴┐ │ Unit Testing │ ← Basic functionality verification └───────────────────┘ ``` --- ## 1. Unit Testing (Foundry) ```solidity contract VaultTest is Test { Vault vault; ERC20Mock token; address alice = makeAddr("alice"); function setUp() public { token = new ERC20Mock(); vault = new Vault(address(token)); token.mint(alice, 1000e18); vm.prank(alice); token.approve(address(vault), type(uint256).max); } function test_deposit() public { vm.prank(alice); vault.deposit(100e18); assertEq(vault.balanceOf(alice), 100e18); } // Test revert function test_RevertWhen_DepositZero() public { vm.prank(alice); vm.expectRevert(Vault.ZeroAmount.selector); vault.deposit(0); } // Test event function test_EmitDeposit() public { vm.expectEmit(true, false, false, true); emit Deposit(alice, 100e18); // Expected event vm.prank(alice); vault.deposit(100e18); } } ``` --- ## 2. Fuzz Testing ```solidity // Foundry auto-generates random inputs (256 runs by default) function testFuzz_deposit(uint256 amount) public { // Constrain input to valid range amount = bound(amount, 1, 1000e18); token.mint(alice, amount); vm.prank(alice); vault.deposit(amount); assertEq(vault.balanceOf(alice), amount); assertEq(token.balanceOf(address(vault)), amount); } // Multi-parameter fuzz function testFuzz_transferAndWithdraw(uint256 depositAmt, uint256 withdrawAmt) public { depositAmt = bound(depositAmt, 1e18, 1000e18); withdrawAmt = bound(withdrawAmt, 1, depositAmt); // Cannot exceed deposit amount vm.startPrank(alice); vault.deposit(depositAmt); vault.withdraw(withdrawAmt); vm.stopPrank(); assertEq(vault.balanceOf(alice), depositAmt - withdrawAmt); } ``` Increase fuzz run count: ```toml # foundry.toml [fuzz] runs = 10000 # Default is 256; 10000+ recommended for high-risk contracts ``` --- ## 3. Invariant Testing (Most Powerful) **Invariants**: Conditions the system must always satisfy regardless of what operations occur. ```solidity // Handler: defines the set of valid operations contract VaultHandler is CommonBase, StdCheats, StdUtils { Vault vault; ERC20Mock token; uint256 public totalDeposited; constructor(Vault _vault, ERC20Mock _token) { vault = _vault; token = _token; } function deposit(uint256 amount) public { amount = bound(amount, 1, 1e30); token.mint(msg.sender, amount); vm.prank(msg.sender); token.approve(address(vault), amount); vm.prank(msg.sender); vault.deposit(amount); totalDeposited += amount; } function withdraw(uint256 amount) public { amount = bound(amount, 0, vault.balanceOf(msg.sender)); if (amount == 0) return; vm.prank(msg.sender); vault.withdraw(amount); totalDeposited -= amount; } } // Invariant test contract contract VaultInvariantTest is Test { Vault vault; VaultHandler handler; function setUp() public { vault = new Vault(...); handler = new VaultHandler(vault, token); targetContract(address(handler)); // Foundry randomly calls handler functions } // Invariant 1: vault token balance = sum of all deposits function invariant_vaultBalanceMatchesDeposits() public { assertEq(token.balanceOf(address(vault)), handler.totalDeposited()); } // Invariant 2: cannot withdraw more than deposited function invariant_noFreeWithdrawal() public { assertGe(handler.totalDeposited(), 0); } } ``` --- ## 4. Fork Testing (Mainnet State) ```solidity contract ForkTest is Test { address constant UNISWAP_V3_POOL = 0x...; address constant WHALE = 0xWHALE_ADDRESS; function setUp() public { vm.createSelectFork(vm.envString("MAINNET_RPC"), 19000000); // Can now interact directly with real contracts } function test_swapOnRealUniswap() public { // Impersonate a whale vm.prank(WHALE); // Interact with real Uniswap V3 ISwapRouter(UNISWAP_ROUTER).exactInputSingle(...); } function test_aaveLiquidation() public { // Simulate price crash → trigger liquidation vm.mockCall(CHAINLINK_ORACLE, abi.encodeWithSelector(latestRoundData.selector), abi.encode(0, 1000e8, 0, block.timestamp, 0)); // ETH price drops to $1000 // Execute liquidation aavePool.liquidationCall(...); } } ``` --- ## Coverage Targets | Contract Type | Target Coverage | |---------|----------| | Core DeFi Logic | **>95%** | | Access Control / Permissions | **100%** | | Utility / Helper Functions | >80% | | View Functions | >70% | ```bash forge coverage --report lcov genhtml lcov.info -o coverage-report ``` ## Pre-Audit Checklist - [ ] All public/external functions have unit tests - [ ] Fuzz tests cover all numeric inputs - [ ] Critical invariants have Invariant tests - [ ] Fork tests verify integration with mainnet protocols - [ ] Gas snapshots compared between versions - [ ] slither / aderyn static analysis shows no high-severity warnings