Formal Verification of Smart Contracts

We design and develop full-cycle blockchain solutions: from smart contract architecture to launching DeFi protocols, NFT marketplaces and crypto exchanges. Security audits, tokenomics, integration with existing infrastructure.
Showing 1 of 1 servicesAll 1306 services
Formal Verification of Smart Contracts
Complex
~1-2 weeks
FAQ
Blockchain Development Services
Blockchain Development Stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1214
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    852
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_logo-advance_0.png
    B2B Advance company logo design
    561
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    823

Formal Verification of Smart Contracts

Formal verification is a mathematical proof of program correctness. Not "we checked all test cases," but "we proved that for any input data the contract behaves according to specification." The difference is critical: tests find presence of errors, verification proves their absence. MakerDAO, Aave, Compound use formal verification for critical components. Let's examine how it works in practice.

Formal Verification Tools

Certora Prover

Certora Prover — the most common tool for EVM smart contracts. Uses its own specification language CVL (Certora Verification Language). Works as SaaS — upload contract and specification, get results.

Specifications are written in CVL:

// Specification for ERC-20 transfer
methods {
    function transfer(address, uint256) external returns (bool) envfree;
    function balanceOf(address) external returns (uint256) envfree;
    function totalSupply() external returns (uint256) envfree;
}

// Invariant: sum of all balances = totalSupply
invariant totalSupplyIsSum(address a, address b)
    a != b =>
    balanceOf(a) + balanceOf(b) <= totalSupply();

// Rule: transfer decreases sender balance
rule transferDecreasesBalance(address sender, address recipient, uint256 amount) {
    require sender != recipient;
    require balanceOf(sender) >= amount;

    uint256 balanceBefore = balanceOf(sender);

    env e;
    require e.msg.sender == sender;
    transfer(e, recipient, amount);

    assert balanceOf(sender) == balanceBefore - amount;
}

// Rule: transfer never creates tokens from thin air
rule noTokenCreation(method f, address a) {
    uint256 totalBefore = totalSupply();

    env e;
    calldataarg args;
    f(e, args);

    assert totalSupply() <= totalBefore;
}

Prover attempts to find counterexample — set of inputs where assertion is violated. If counterexample not found in given time — specification considered proven.

Solidity SMTChecker

Built into Solidity compiler, based on SMT (Satisfiability Modulo Theories). Activated through pragma or compiler flags:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
// Enable SMT checking
/// @custom:smtchecker abstract-function-nondet

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

    function deposit(uint256 amount) external {
        require(amount > 0, "Zero amount");

        // SMTChecker automatically verifies:
        // - no overflow in balances[msg.sender] += amount
        // - no overflow in totalDeposited += amount
        // - invariant: totalDeposited = Σ balances[i]

        balances[msg.sender] += amount;
        totalDeposited += amount;
    }

    function withdraw(uint256 amount) external {
        require(balances[msg.sender] >= amount, "Insufficient balance");

        balances[msg.sender] -= amount;
        totalDeposited -= amount;
        // assert not needed — SMTChecker verifies underflow impossible
    }
}

Run through Hardhat:

npx hardhat compile --config hardhat.smtchecker.config.ts
// hardhat.smtchecker.config.ts
export default {
    solidity: {
        version: "0.8.20",
        settings: {
            modelChecker: {
                engine: "chc",        // Constrained Horn Clauses — most powerful
                targets: [
                    "assert",
                    "overflow",
                    "underflow",
                    "divByZero",
                    "constantCondition",
                    "balance"
                ],
                timeout: 20000,       // milliseconds per check
                showUnproved: true
            }
        }
    }
};

Halmos

Halmos — symbolic execution for EVM, written in Python. Works with existing Foundry tests — symbolically executes them for all possible inputs:

// Foundry test becomes formal verification in Halmos
// Run: halmos --contract TestVault --function testFormal

contract TestVault is Test {
    Vault vault;

    function setUp() public {
        vault = new Vault(address(token));
    }

    // Halmos runs this for ALL possible values of amount and caller
    function testFormal_depositWithdraw(
        uint256 amount,
        address caller
    ) public {
        vm.assume(amount > 0 && amount < type(uint128).max);
        vm.assume(caller != address(0));

        deal(address(token), caller, amount);

        vm.prank(caller);
        token.approve(address(vault), amount);

        vm.prank(caller);
        vault.deposit(amount);

        uint256 shares = vault.balanceOf(caller);

        vm.prank(caller);
        vault.withdraw(shares);

        // For all possible amount and caller: after deposit+withdraw
        // balance recovers (accounting for fees)
        assertGe(token.balanceOf(caller), amount * 99 / 100);
    }
}

Specification: The Most Difficult Part

Tools are means. Main work is writing specification. Bad specification proves contract correct according to wrong requirements.

Types of Properties for Verification

Safety properties ("bad never happens"):

  • Balance never goes negative
  • totalSupply never exceeds MAX_SUPPLY
  • Only owner can call pause()
  • Reentrancy guard works correctly

Liveness properties ("good eventually happens"):

  • If user deposits funds, they can withdraw them
  • Proposals eventually execute or reject
  • Staker eventually receives rewards

Invariants ("always true"):

  • Σ balances = totalSupply (conservation of tokens)
  • lockedAmount <= totalDeposited
  • Oracle price always > 0

Example Complete Specification for Lending Protocol

// Specification for simplified Aave-like protocol

methods {
    function deposit(uint256) external envfree;
    function borrow(uint256) external envfree;
    function repay(uint256) external envfree;
    function liquidate(address) external;
    function getHealthFactor(address) external returns (uint256) envfree;
    function collateral(address) external returns (uint256) envfree;
    function debt(address) external returns (uint256) envfree;
}

// Invariant: cannot liquidate healthy borrower
rule noLiquidationOfHealthyBorrower(address borrower) {
    require getHealthFactor(borrower) >= 1e18; // health factor >= 1.0

    env e;
    liquidate@withrevert(e, borrower);

    assert lastReverted, "Healthy borrower should not be liquidatable";
}

// Invariant: sum of debts doesn't exceed sum of collateral (with LTV coefficient)
invariant solvencyInvariant(address user)
    debt(user) * 100 <= collateral(user) * MAX_LTV_PERCENT
    filtered { f -> !f.isView }

// Reentrancy: state cannot change twice in one transaction
rule noReentrancy(method f) {
    uint256 collateralBefore = collateral(currentContract);

    env e;
    calldataarg args;
    f(e, args);

    uint256 collateralAfter = collateral(currentContract);

    // Balance changed exactly as expected
    assert collateralAfter >= collateralBefore || /* withdrawal */
           collateralAfter <= collateralBefore; // deposit
}

Limitations of Formal Verification

Formal verification is not a silver bullet:

Completeness gap: only what's specified in specification is verified. If attacker finds vector not covered by specification — verification won't catch it. This happened with some "verified" protocols.

Scalability: large contracts (> 1000 lines) are difficult to verify completely. Tools may not finish in reasonable time (timeout). Solution — verify critical components separately.

Oracle assumptions: if contract uses oracle, verification assumes oracle returns correct data. Incorrect oracle — outside verification scope.

External calls: interaction with external contracts is difficult to specify completely. Certora requires explicit specification of external behavior through summarization.

Verification Audit Process

Full formal verification process:

Stage 1 — Requirements Specification: jointly with protocol team define critical properties that must hold under any conditions.

Stage 2 — CVL/Halmos Specification Writing: 2-4 weeks for typical DeFi protocol. Cover safety invariants, access control, economic invariants.

Stage 3 — Iterative Verification: run Prover, analyze counterexamples, refine specification or fix code. This is iterative process.

Stage 4 — Report: list of verified properties, found violations, verification limitations.

Typical cost of verification audit — $50k-$200k depending on protocol complexity. Significantly more than usual audit, but justified for contracts with $100M+ TVL: cost of finding one critical vulnerability worth $10k in specification can prevent $10M loss.

Check Type Finds Cost Time
Unit tests Specific scenarios Low 1-2 weeks
Fuzz testing Random inputs Low 1 week
Manual audit Logic errors Medium 2-4 weeks
Formal verification Mathematical proof High 4-8 weeks

Formal verification does not replace manual audit — they complement each other. Manual audit finds logic errors in business logic, verification proves correctness of mathematical properties.