DEV Community

ohmygod
ohmygod

Posted on

Formal Verification for DeFi Developers: Halmos vs Certora vs HEVM — When Fuzzing Isn't Enough

Fuzzing catches bugs. Formal verification proves their absence. After auditing protocols that collectively lost $200M+ in 2026, I've seen the same pattern: teams that fuzz find 80% of issues, but the remaining 20% — the ones that drain millions — hide in mathematical edge cases that random inputs never reach.

This is a practical comparison of three formal verification tools I use in real DeFi audits: Halmos, Certora Prover, and HEVM. No theory-only fluff — every example comes from real vulnerability patterns.

Why Fuzzing Falls Short for DeFi

Consider this simplified lending pool:

function withdraw(uint256 shares) external {
    uint256 assets = shares * totalAssets() / totalSupply();
    _burn(msg.sender, shares);
    token.transfer(msg.sender, assets);
}
Enter fullscreen mode Exit fullscreen mode

A fuzzer testing random shares values might never hit the exact state where totalSupply() is 1 and totalAssets() is type(uint256).max — creating a first-depositor inflation attack. Formal verification explores all possible states symbolically and would flag this immediately.

Tool 1: Halmos (a16z) — Symbolic Testing With Your Existing Tests

Halmos is the lowest-friction entry point. If you already write Foundry tests, you're 90% there.

Installation

# Recommended: using uv
curl -LsSf https://astral.sh/uv/install.sh | sh
uv tool install --python 3.12 halmos

# Or pip
pip install halmos
Enter fullscreen mode Exit fullscreen mode

Converting a Fuzz Test to Formal Verification

Here's a standard Foundry fuzz test:

function testFuzz_withdrawNeverExceedsBalance(uint256 shares) public {
    vm.assume(shares > 0 && shares <= vault.balanceOf(address(this)));
    uint256 balBefore = token.balanceOf(address(this));
    vault.withdraw(shares);
    assertGe(token.balanceOf(address(this)), balBefore);
}
Enter fullscreen mode Exit fullscreen mode

To make Halmos verify this for all possible inputs, just rename it:

function check_withdrawNeverExceedsBalance(uint256 shares) public {
    // Halmos treats 'shares' as a symbolic variable
    // covering ALL possible uint256 values simultaneously
    vm.assume(shares > 0 && shares <= vault.balanceOf(address(this)));
    uint256 balBefore = token.balanceOf(address(this));
    vault.withdraw(shares);
    assertGe(token.balanceOf(address(this)), balBefore);
}
Enter fullscreen mode Exit fullscreen mode

Run it:

halmos --function check_withdrawNeverExceedsBalance
Enter fullscreen mode Exit fullscreen mode

If the assertion can be violated under any input, Halmos returns a concrete counterexample. If it passes, you have mathematical proof (within bounds) that the property holds.

When Halmos Shines

  • Arithmetic edge cases: Overflow, rounding, precision loss
  • Access control completeness: Proving only authorized roles can call functions
  • State transition correctness: Verifying invariants hold across all function calls

Halmos Limitations

  • Bounded verification: Loops unroll to a fixed depth (default: configurable with --loop), so infinite-loop contracts need careful bounds
  • No cross-contract symbolic storage: External calls are treated as opaque unless you model them
  • Performance: Complex contracts with deep call stacks can hit solver timeouts

Tool 2: Certora Prover — Industrial-Grade Specification Language

Certora is the most powerful tool here, but demands the most investment. It uses CVL (Certora Verification Language), a dedicated specification language separate from Solidity.

Writing a CVL Specification

// TotalSupplyIntegrity.spec
methods {
    function totalSupply() external returns (uint256) envfree;
    function balanceOf(address) external returns (uint256) envfree;
    function deposit(uint256) external;
    function withdraw(uint256) external;
}

// Invariant: sum of all balances equals totalSupply
invariant totalSupplyIsSumOfBalances()
    to_mathint(totalSupply()) == 
    sum(address a. to_mathint(balanceOf(a)));

// Rule: no single operation should drain more than deposited
rule withdrawCannotExceedDeposit(uint256 depositAmt, uint256 withdrawAmt) {
    env e;

    uint256 balBefore = balanceOf(e.msg.sender);
    deposit(e, depositAmt);
    withdraw(e, withdrawAmt);
    uint256 balAfter = balanceOf(e.msg.sender);

    assert balAfter >= balBefore - depositAmt,
        "Withdraw drained more than deposit";
}
Enter fullscreen mode Exit fullscreen mode

Real-World Example: Oracle Manipulation Detection

This CVL spec would have caught the Aave oracle misconfiguration:

// OracleSafety.spec
rule priceChangeWithinBounds() {
    env e1; env e2;

    uint256 priceBefore = getAssetPrice(e1);
    // Allow any state changes between observations
    uint256 priceAfter = getAssetPrice(e2);

    // Price should never change more than 50% in a single block
    assert priceAfter <= priceBefore * 150 / 100,
        "Price spike exceeds safety bound";
    assert priceAfter >= priceBefore * 50 / 100,
        "Price crash exceeds safety bound";
}
Enter fullscreen mode Exit fullscreen mode

When Certora Shines

  • Cross-function invariants: Verifying properties that span multiple function calls
  • Protocol-level properties: "Total value locked can never decrease except through legitimate withdrawals"
  • Governance safety: Proving timelock and multisig constraints are enforced

Certora Limitations

  • Learning curve: CVL is a new language; expect 2-4 weeks to become productive
  • Cloud-based: Proofs run on Certora's infrastructure (privacy consideration)
  • Cost: Free tier available, but production use requires a license

Tool 3: HEVM (DappTools/Foundry) — Lightweight Symbolic Execution

HEVM is a symbolic EVM executor that integrates with the DappTools/Foundry ecosystem. It's lighter than Certora but more capable than basic Halmos for certain patterns.

Installation

# Via Nix (recommended)
nix-env -i hevm

# Or from source
git clone https://github.com/ethereum/hevm && cd hevm
cabal install
Enter fullscreen mode Exit fullscreen mode

Equivalence Checking

HEVM excels at proving two implementations produce identical results — critical when upgrading contracts:

# Prove that VaultV2.withdraw() behaves identically to VaultV1.withdraw()
hevm equivalence \
  --code-a $(forge inspect VaultV1 bytecode) \
  --code-b $(forge inspect VaultV2 bytecode) \
  --sig "withdraw(uint256)"
Enter fullscreen mode Exit fullscreen mode

If any input produces different outputs, HEVM gives you the exact calldata that diverges.

Symbolic Execution of Specific Functions

# Symbolically execute withdraw() and check for assertion violations
hevm symbolic \
  --code $(forge inspect Vault bytecode) \
  --sig "withdraw(uint256)" \
  --assertions '[0x01]'
Enter fullscreen mode Exit fullscreen mode

When HEVM Shines

  • Upgrade safety: Proving V2 contracts are backward-compatible
  • Quick property checks: Faster setup than Certora for single-function properties
  • Bytecode-level analysis: Works on deployed contracts without source code

HEVM Limitations

  • Less expressive specs: No dedicated specification language
  • Smaller community: Less documentation and fewer examples than Halmos or Certora
  • Haskell dependency: Nix installation can be complex on some systems

Head-to-Head Comparison

Setup Time:

  • Halmos: Minutes (pip install, rename test prefix)
  • HEVM: Hours (Nix/Haskell setup)
  • Certora: Days (learn CVL, configure cloud runner)

Expressiveness:

  • Halmos: Medium (Solidity assertions only)
  • HEVM: Medium (bytecode-level, equivalence proofs)
  • Certora: High (dedicated CVL with quantifiers, invariants, ghost variables)

Best For:

  • Halmos: Teams already using Foundry; arithmetic/access control properties
  • HEVM: Contract upgrades; bytecode equivalence; quick checks
  • Certora: High-value DeFi protocols; complex cross-function invariants; governance

Cost:

  • Halmos: Free (open source, a16z)
  • HEVM: Free (open source, Ethereum Foundation)
  • Certora: Free tier + paid plans

My Recommended Pipeline for DeFi Audits

After integrating formal verification into 15+ audits, here's the pipeline that catches the most bugs per hour invested:

Phase 1: Slither static analysis (5 min setup)
    ↓ catches low-hanging fruit
Phase 2: Foundry fuzz tests (existing test suite)
    ↓ catches common edge cases
Phase 3: Halmos symbolic tests (rename check_*, 30 min)
    ↓ proves arithmetic properties for ALL inputs
Phase 4: Certora CVL specs (2-4 days for critical paths)
    ↓ proves protocol-level invariants
Phase 5: HEVM equivalence (if upgrading contracts)
    ↓ proves backward compatibility
Enter fullscreen mode Exit fullscreen mode

The key insight: Phases 1-3 are nearly free if you already have Foundry tests. Phase 4 is where you invest for high-value protocols. Phase 5 is situational but invaluable for upgrades.

Real Bugs Formal Verification Would Have Caught

  1. Solv Protocol ($2.7M): A Certora invariant totalMinted <= totalDeposited * MAX_RATIO would have flagged the double-mint path before deployment.

  2. Aave Oracle Misconfig ($26M): A Halmos check_ test asserting priceChange <= MAX_DEVIATION per block would have caught the CAPO rate-of-change desync.

  3. First Depositor Attacks (multiple protocols): The Halmos test at the top of this article — check_withdrawNeverExceedsBalance — catches this class of bug in under 10 seconds.

Getting Started Today

  1. Install Halmos and rename one fuzz test to check_ — see if it finds anything new
  2. Write three invariants for your protocol's core accounting (total supply, total assets, share price monotonicity)
  3. If managing >$10M TVL, invest in Certora specs for your critical paths
  4. If upgrading contracts, run HEVM equivalence checks before deployment

Formal verification isn't a silver bullet — it proves properties you specify, not properties you forget to specify. But combined with fuzzing, static analysis, and expert review, it's the closest thing we have to mathematical certainty in DeFi security.


DreamWork Security researches DeFi vulnerabilities and builds open-source security tools. Follow for weekly deep dives into smart contract security.

Top comments (0)