Best Practices

Testing Smart Contracts for Security Vulnerabilities: Beyond Unit Tests

Kennedy OwiroOctober 27, 20259 min read

Unit tests verify that code works correctly with expected inputs. Security testing verifies that code works correctly with adversarial inputs — and that's a fundamentally different challenge. Fuzz testing, invariant testing, and formal verification find bugs that unit tests never will. Here's how to add each to your workflow.

The Testing Pyramid for Smart Contracts

  1. Unit Tests — Verify individual functions with known inputs/outputs
  2. Integration Tests — Verify contract interactions work correctly
  3. Fuzz Tests — Throw random inputs at functions to find edge cases
  4. Invariant Tests — Verify properties that should ALWAYS be true
  5. Formal Verification — Mathematically prove correctness properties

Fuzz Testing with Foundry

Foundry's fuzzer generates random inputs and runs your test function thousands of times, looking for inputs that cause failure.

// Foundry fuzz test — runs with random inputs
function testFuzz_depositAndWithdraw(uint256 amount) public {
    amount = bound(amount, 1, 1000 ether);  // Constrain to valid range

    deal(address(token), alice, amount);
    vm.startPrank(alice);
    token.approve(address(vault), amount);
    vault.deposit(amount);

    uint256 shares = vault.balanceOf(alice);
    vault.withdraw(shares);

    // User should get back at least their deposit (minus fees)
    assertGe(token.balanceOf(alice), amount * 99 / 100);
    vm.stopPrank();
}

Invariant Testing

Invariant tests define properties that must always hold, then let the fuzzer call random sequences of functions trying to break them.

// This property must ALWAYS be true:
function invariant_vaultSolvency() public {
    assertGe(
        token.balanceOf(address(vault)),
        vault.totalAssets(),
        "Vault is insolvent!"
    );
}

function invariant_totalSharesMatchIndividual() public {
    uint256 sum;
    for (uint i = 0; i < actors.length; i++) {
        sum += vault.balanceOf(actors[i]);
    }
    assertEq(vault.totalSupply(), sum);
}

What Each Level Catches

Testing LevelCatchesMisses
Unit testsBasic logic errors, simple regressionsEdge cases, state-dependent bugs
Fuzz testsBoundary errors, overflow, input validationMulti-step exploits, economic attacks
Invariant testsState corruption, accounting bugs, solvencyExternal integration bugs
Formal verificationMathematical proof of correctnessRequires specification (what IS correct?)

Minimum Security Testing Checklist

  • ✅ Unit tests for all functions (happy path + error cases)
  • ✅ Fuzz tests for any function that takes numeric inputs
  • ✅ Invariant tests for core accounting (solvency, supply matching)
  • ✅ Test with extreme values: 0, 1, type(uint256).max
  • ✅ Test as both EOA and contract callers
  • ✅ Test reentrancy scenarios with malicious receiver contracts

Testing catches bugs before they reach production. Auditing catches the ones testing missed. Get both with Vultbase.

testingfuzz testinginvariant testingFoundryHardhatformal verification
Share

Written by

Kennedy Owiro

Founder & CTO, Vultbase

14+ years building security and QA systems at scale. Background in fintech security and Web3 smart contract testing. Built Vultbase's Intelligence Engine with 1,200+ exploit patterns from $40B+ in historical DeFi losses.

Protect your protocol before launch.

Submit your smart contracts for automated security analysis powered by 1,200+ real exploit patterns.

Start Your Audit →