Skip to content

Formal Verification of Smart Contracts — Solidity, Slither and Certora

DodaTech Updated 2026-06-23 6 min read

In this tutorial, you'll learn about Formal Verification of Smart Contracts. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

Formal Verification of smart contracts applies mathematical reasoning to Solidity bytecode to prove the absence of reentrancy, arithmetic overflow, and access control vulnerabilities before deployment on Ethereum.

Learning Path

flowchart LR
  A["Hoare Logic"] --> B["Smart Contract Verification
Slither, Foundry, Certora"] B --> C["Model Checking"] B --> D["Industrial Formal Verification"] style B fill:#f90,color:#fff,stroke-width:2px
â„šī¸ Info

What you'll learn: Writing formal specifications for Solidity contracts, using Slither for automated detection, Foundry for property-based fuzzing, and Certora Prover for full Formal Verification.

Why it matters: Smart contract exploits have caused billions in losses. Formal Verification mathematically guarantees that contracts behave correctly for all possible inputs and states.

Real-world use: Durga Antivirus Pro uses Symbolic Execution to verify that smart contract audit findings are reproducible, confirming exploit paths before reporting them to developers.

Prerequisites

Basic Solidity programming, Ethereum fundamentals, and some exposure to Formal Verification concepts.

Why Verify Smart Contracts Formally?

Smart contracts are immutable once deployed, making pre-deployment verification critical. A single bug can lead to irreversible loss of funds. Traditional testing covers only specific input values, while Formal Verification proves properties for all possible states and transactions.

Key properties to verify: no reentrancy, correct arithmetic, proper access control, invariant preservation, and total supply consistency.

Step-by-Step: Static Analysis with Slither

Step 1: Install Slither

pip install slither-analyzer

Step 2: Create a Vulnerable Contract

// VulnerableBank.sol
pragma solidity ^0.8.0;

contract VulnerableBank {
    mapping(address => uint256) public balances;

    function deposit() external payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw(uint256 amount) external {
        require(balances[msg.sender] >= amount);
        (bool sent, ) = msg.sender.call{value: amount}("");
        require(sent);
        balances[msg.sender] -= amount;  // Reentrancy: state after call
    }
}

Step 3: Run Slither

slither VulnerableBank.sol --detect reentrancy

Expected output:

VulnerableBank.withdraw(uint256) (VulnerableBank.sol#10-16) uses a potential reentrancy via external call:
    - (bool sent,) = msg.sender.call{value: amount}("") (VulnerableBank.sol#12)
    - balances[msg.sender] -= amount (VulnerableBank.sol#14)

Step-by-Step: Property-Based Testing with Foundry

Step 1: Create a Foundry Project

forge init my-project
cd my-project

Step 2: Write a Contract with Properties

// src/Token.sol
pragma solidity ^0.8.0;

contract Token {
    mapping(address => uint256) public balanceOf;
    uint256 public totalSupply;

    constructor(uint256 _initialSupply) {
        balanceOf[msg.sender] = _initialSupply;
        totalSupply = _initialSupply;
    }

    function transfer(address to, uint256 amount) external {
        require(balanceOf[msg.sender] >= amount);
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
    }
}

Step 3: Write a Formal Invariant Test

// test/Token.t.sol
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
import "../src/Token.sol";

contract TokenTest is Test {
    Token token;

    function setUp() public {
        token = new Token(1000);
    }

    function testTotalSupplyInvariant() public {
        uint256 supply = token.totalSupply();
        vm.assume(token.balanceOf(address(0x1)) <= supply);
        // Fuzz: totalSupply must never change
        assertEq(token.totalSupply(), supply);
    }

    function testTransferInvariant(uint256 amount) public {
        address alice = address(0x1);
        vm.assume(alice != address(0));
        vm.assume(amount > 0 && amount <= token.balanceOf(alice));
        uint256 aliceBefore = token.balanceOf(alice);
        uint256 bobBefore = token.balanceOf(address(0x2));
        vm.prank(alice);
        token.transfer(address(0x2), amount);
        assertEq(token.balanceOf(alice), aliceBefore - amount);
        assertEq(token.balanceOf(address(0x2)), bobBefore + amount);
    }
}

Step 4: Run Foundry Verification

forge test

Expected output:

[PASS] testTotalSupplyInvariant() (gas: 4213)
[PASS] testTransferInvariant(uint256) (runs: 256, Îŧ: 6132, ~: 6521)

Step-by-Step: Certora Prover

Step 1: Write a Specification Rule

// token.spec
rule total_supply_invariant() {
    uint256 supply_before = totalSupply;
    storage var store = getStorage();

    // Any arbitrary function call
    calldataarg txData;
    uint256 value;
    address sender;
    // Invoke the contract
    processTransaction(txData, sender, value);

    uint256 supply_after = totalSupply;
    assert supply_before == supply_after,
        "Total supply must never change";
}

rule transfer_consistency(address to, uint256 amount) {
    require to != currentContract;
    uint256 sender_before = balanceOf[currentContract];
    uint256 recipient_before = balanceOf[to];
    require sender_before >= amount;

    transfer@withrevert(to, amount);

    uint256 sender_after = balanceOf[currentContract];
    uint256 recipient_after = balanceOf[to];
    assert sender_after == sender_before - amount,
        "Sender balance decreased correctly";
    assert recipient_after == recipient_before + amount,
        "Recipient balance increased correctly";
}

Step 2: Run Certora

certoraRun Token.sol --verify Token:token.spec

Common Errors

1. Forgetting to Check Reentrancy After External Calls

State changes after external calls are vulnerable to reentrancy. Follow the checks-effects-interactions pattern: update state before making external calls.

2. Integer Overflow in Solidity < 0.8

Solidity versions before 0.8 do not automatically check for overflow. Use SafeMath or upgrade to 0.8+.

3. Incomplete Access Control Verification

OnlyOwner modifiers can miss edge cases. Verify that every state-changing function has the correct access control using formal specifications.

4. Assuming External Contracts Are Safe

Formal Verification should assume worst case: the external contract can call back into your contract at any point. Model this in your specifications.

5. Insufficient Fuzzing Ranges

Property-based tests with too few runs may miss edge cases. Foundry's default 256 runs may need to be increased for complex contracts.

Practice Questions

Q1: What is a reentrancy attack?

When a contract calls an external contract that calls back into the original contract before the first invocation completes, exploiting stale state.

Q2: How does Slither detect vulnerabilities?

Slither builds a control-flow graph and data-dependency graph, then applies detection rules based on patterns of state changes and external calls.

Q3: What is a formal invariant for a token contract?

Total supply must remain constant across all transfers. No function should create or destroy tokens unless explicitly designed to do so.

Q4: How does Foundry's fuzzing verify properties?

Foundry generates random inputs constrained by vm.assume() and checks that assertions hold across many runs, providing statistical coverage.

Q5: What does the Certora Prover prove that testing cannot?

Certora proves properties hold for ALL possible inputs and states, not just the specific values tested by fuzzers.

Challenge

Write a Solidity vault contract that allows users to deposit and withdraw ERC20 tokens. Use Foundry to write property-based tests proving that: total deposited tokens never exceeds contract balance, no user can withdraw more than they deposited, and the sum of all user balances equals the contract's token balance.

FAQ

### Is Formal Verification mandatory for DeFi?

Not yet, but many protocols require audits that include Formal Verification. The Ethereum Foundation funds Formal Verification research for critical infrastructure.

### Can Formal Verification find all smart contract bugs?

Only bugs that violate the specified properties. If the specification is wrong or incomplete, the verification may confirm a flawed contract.

### How long does Certora verification take?

Simple contracts verify in minutes. Complex contracts with many paths may take hours. Rule parallelization and abstraction help manage complexity.

### What is the checks-effects-interactions pattern?

Update contract state before making external calls. This prevents reentrancy because the attacker sees the updated state when they call back.

### Does Formal Verification replace manual auditing?

No. Formal Verification and manual auditing are complementary. Verification proves specified properties; auditors find specification gaps and economic attacks.


Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro