Formal Verification for DeFi 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 1All 1305 services
Formal Verification for DeFi Smart Contracts
Complex
~1-2 weeks
Frequently Asked Questions

Blockchain Development Services

Blockchain Development Stages

Latest works

  • image_website-b2b-advance_0.webp
    B2B ADVANCE company website development
    1347
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1247
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    948
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1183
  • image_logo-advance_0.webp
    B2B Advance company logo design
    642
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    921

A $10M loss from a reentrancy vulnerability could have been prevented by formal verification. We implement mathematical proof of correctness for critical DeFi protocols. The difference is critical: tests find presence of bugs, verification proves their absence. MakerDAO, Aave, and Compound use formal verification for critical components. Let's see how it works in practice.

Formal verification is not just an audit—it's mathematical proof. It guarantees that for any input data, the contract behaves correctly. According to Certora research, formal verification covers 100% of possible execution paths, while fuzz testing covers only 60%—making it 1.67 times more effective in path coverage. For finding reentrancy vulnerabilities, it is 5 times more effective than a standard audit.

Why Formal Verification Is Not Testing

Tests check specific scenarios; verification checks all possible inputs. Certora Prover looks for a counterexample—a set of data where an assertion is violated. If no counterexample is found within a given time (e.g., 20 seconds), the property is considered proven. This provides a guarantee that even fuzz testing cannot match.

Certora Prover

Certora Prover is the most common tool for EVM smart contracts. It uses its own specification language, CVL (Certora Verification Language). It works as a SaaS—upload your contract and specification, get results.

Specification is 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's 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 out of 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 a counterexample. If none found, the specification is considered proven.

Solidity SMTChecker

Built into the Solidity compiler, based on SMT (Satisfiability Modulo Theories). Activated via 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");
        balances[msg.sender] += amount;
        totalDeposited += amount;
    }

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

Run via modelChecker setting in Hardhat. SMTChecker automatically checks overflows, underflows, and invariants.

Halmos — Symbolic Execution for Foundry

Halmos symbolically executes existing Foundry tests for all possible input data:

contract TestVault is Test {
    Vault vault;

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

    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);
        assertGe(token.balanceOf(caller), amount * 99 / 100);
    }
}

How Specification Prevents Vulnerabilities

Tools are just means. The main work is writing the specification. A bad specification will prove that the contract is correct according to wrong requirements. That's why we focus on formalizing business logic.

Types of Properties for Verification

Safety properties ("bad thing never happens"):

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

Liveness properties ("good thing eventually happens"):

  • If a user deposits funds, they can withdraw them
  • Proposals eventually get executed or rejected
  • Staker eventually receives rewards

Invariants ("always true"):

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

Example Specification for a Lending 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 a healthy borrower
rule noLiquidationOfHealthyBorrower(address borrower) {
    require getHealthFactor(borrower) >= 1e18;
    env e;
    liquidate@withrevert(e, borrower);
    assert lastReverted, "Healthy borrower should not be liquidatable";
}

// Invariant: total debt does not exceed total collateral
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);
    assert collateralAfter >= collateralBefore ||
           collateralAfter <= collateralBefore;
}

What's Included in Our Service

We offer a full cycle of formal verification for your contract. Our process consists of four steps: (1) requirements specification, (2) writing CVL rules, (3) iterative verification, and (4) report preparation. As a result, you get:

  • Documentation in the form of a CVL specification for all critical properties
  • Execution of Certora Prover (or Halmos) with a detailed report
  • A list of verified properties and any violations found
  • Support in fixing counterexamples
  • Guarantee that the properties are mathematically proven

Our experience: 5+ years in blockchain development, 15+ smart contract audits, 3 protocols verified with TVL > $200M. Contact us to evaluate your project. Typical cost: $10,000–$50,000 depending on complexity, often saving millions in potential losses. Order formal verification in 4–8 weeks.

Limitations of Formal Verification

Formal verification is not a silver bullet:

  • Completeness gap: Only what is specified is verified. If an attacker finds a vector not covered by the specification, verification won't catch it.
  • Scalability: Large contracts (>1000 lines) are hard to verify fully. Solution: verify critical components separately.
  • Oracle assumptions: If the contract uses an oracle, verification assumes the oracle returns correct data.
  • External calls: Interactions with external contracts are difficult to specify completely.

Comparison of Verification Methods

Type of Check What It Finds Cost Time
Unit tests Specific scenarios Low 1–2 weeks
Fuzz testing Random input data Low 1 week
Manual audit Logical errors Medium 2–4 weeks
Formal verification Mathematical proof High ($10k–$50k) 4–8 weeks

Comparison of Formal Verification Tools

Tool Type Specification Language Ease of Adoption Coverage
Certora Prover Model checking CVL Medium Full for EVM
SMTChecker SMT-solving Solidity annotations Low Automatic
Halmos Symbolic execution Foundry tests Medium Depends on tests

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

Frequently Asked Questions (click to expand)
  • Formal verification differs from a regular audit in that it proves the absence of bugs, not just finds them. For critical contracts (e.g., lending protocols), it is the only way to guarantee no hidden vulnerabilities for any input data.
  • A full verification typically takes 4 to 8 weeks depending on protocol complexity, including requirements specification, writing CVL rules, iterative verification, and report preparation.
  • Tools used include Certora Prover for EVM contracts, Halmos for symbolic execution, and the built-in SMTChecker in Solidity. Choice depends on contract size and required depth.
  • Already deployed contracts can be verified if source code is available; however, fixing errors after deployment requires a proxy upgrade or migration, so verifying before deployment is recommended.
  • Guarantees include mathematical proof for all specified properties. If an error is found after verification, the specification is fixed and correctness is reproven at no extra cost. The team has 5+ years of blockchain development experience and 15+ successful audits.

Contact us to discuss your project. With a track record of 15+ successful audits and 5+ years in blockchain security, we provide mathematical proof of security for your smart contract.

How Do We Find What the Compiler Misses?

When a protocol loses $197M through a flash loan attack on a function that auditors reviewed live — it's not an accident. It's a systemic gap in methodology. Our experience shows: vulnerabilities live in a contract for over a year, while the compiler remains silent. We restructured the audit process to catch such cases before deployment.

What Static Analysis Won't Find?

Slither is the standard first tool. It finds reentrancy, integer overflow (in older Solidity versions), improper use of tx.origin, variable shadowing, uninitialized storage. On a real project, Slither produces dozens of warnings, of which critical ones are 0‑2. The rest is informational noise.

Slither won't find logical vulnerabilities. If withdraw correctly checks balance and correctly updates state, but business logic allows double deduction through two different code paths — Slither stays silent.

Mythril uses symbolic execution: builds a graph of all possible execution paths and searches for reachable states violating properties. Works well on isolated contracts. On a protocol of 20 contracts with cross‑contract calls — path explosion, analysis hangs or returns false positives.

Both tools are mandatory as a first pass. But they don't replace manual analysis.

Fuzzing: Where Echidna and Foundry Find Real Bugs

Echidna is a property‑based fuzzer from Trail of Bits. The idea: formulate contract invariants as Solidity functions (echidna_invariant), Echidna generates random call sequences and tries to break the invariant.

Example invariant for a lending protocol:

function echidna_total_assets_ge_liabilities() public view returns (bool) {
    return totalAssets() >= totalLiabilities();
}

Echidna will find a sequence deposit → borrow → liquidate → repay that violates this invariant. You can't build such a case manually — too many combinations.

Foundry fuzzing (forge test --fuzz-runs 100000) is easier to integrate if the team is already on Foundry. Supports stateful fuzzing via invariant tests. In a real project: auditing a vault contract, Foundry fuzzed for 40 minutes and found an edge case where maxWithdraw returned a value larger than actual balance at a specific shares/assets ratio after several donations. Hardhat unit tests missed it — they didn't have that combination of parameters.

Medusa (from Trail of Bits, newer than Echidna) supports corpus‑guided fuzzing and runs faster on large contracts. If the codebase exceeds 5000 lines of Solidity — we look at Medusa.

How Invariants Help Identify Critical Vulnerabilities

Formal verification proves that the contract satisfies specifications for all possible inputs — not for N random ones, but mathematically for all. Tools: Certora Prover, K Framework, Halmos.

Certora works with CVL (Certora Verification Language): write rules and invariants, the Prover translates them into SMT formulas and checks via Z3/CVC5. MakerDAO, Aave, Uniswap use Certora in CI/CD pipeline — every PR is automatically verified.

Limitations: doesn't work with unbounded loops, struggles with hash functions and signature verification. For contracts with simple math (AMM, lending) — excellent. For contracts with arbitrary external calls — difficult to write sufficiently complete specifications.

Formal verification makes sense for contracts that: manage over $50M, are rarely updated, have clearly formalizable invariants. For fast‑iterating products — the cost‑benefit ratio doesn't favor verification.

What Attack Vectors Do Junior Auditors Miss?

Storage collision in proxy pattern. Transparent proxy and UUPS use specific slots for implementation address (EIP‑1967). If an implementation accidentally declares a variable in slot 0 that overlaps with proxy storage — we get silent override. Slither won't catch this if proxy and implementation are in different files.

Read‑only reentrancy. Classic reentrancy guard protects against state changes during recursive calls. But if an external contract reads state via a view function mid‑transaction — guard doesn't help. Years ago, Curve pools became an attack vector precisely through this: an external protocol read get_virtual_price during a reentrancy‑vulnerable state of Curve.

Oracle manipulation via TWAP. Spot price is a standard target for flash loan attack. TWAP is harder to manipulate, but not impossible: on low‑liquidity Uniswap v2 pairs, TWAP can be shifted over several blocks with enough capital. Proper protection: use Chainlink as primary oracle with TWAP as fallback, with deviation threshold check.

Gas griefing on unbounded loop. A function iterates over an array of users. Attacker adds thousands of addresses with zero balances — the function's gas cost rises to the gas limit, making it inaccessible. Protection: pull pattern instead of push, limit array lengths, batch processing with position tracking.

Front‑running on MEV. Transaction is visible in mempool before inclusion in block. MEV bot sees addLiquidity for a significant amount, inserts its own swap before it (sandwich attack). For AMM this is part of the model. For protocols with price functions — require minAmountOut / deadline parameter and its mandatory verification.

Structure of a Full Audit

  1. Scope definition and automated analysis (1‑2 days). Fix commit hash, compiler version, list of out‑of‑scope items. Run Slither, Mythril, Aderyn. Triage: separate real critical bugs from false positives. Build contract dependency map.

  2. Manual analysis (5‑15 days). Each contract line by line. Special attention: all external and public functions, all transfer/call/delegatecall, all places where state changes before a check or after an external call, all math operations with user inputs. On average, 95% of found vulnerabilities are logical, not technical.

  3. Fuzzing and testing (2‑5 days). Echidna or Foundry invariant tests for critical invariants. Fork mainnet tests — verify behavior in real environment with real oracles. For example, in 4 days fuzzing finds on average 3 edge cases not covered by unit tests.

  4. Report and mitigation. Report with severity (Critical/High/Medium/Low/Informational), attack vector description, PoC code for Critical/High. Developers fix, auditors perform re‑audit of fixes.

Severity Examples Requires re‑audit?
Critical Drain funds, unauthorized ownership transfer Always
High Manipulation, DoS on key functions Always
Medium Incorrect behavior on edge cases Recommended
Low Gas inefficiency, typos in events Optional

Audit in CI/CD

Common practice for mature protocols: Slither and Aderyn run in GitHub Actions on every PR. Certora Prover — on merge to main. This doesn't replace a full audit before deployment, but catches regressions.

# .github/workflows/audit.yml
- name: Run Slither
  uses: crytic/[email protected]
  with:
    target: 'src/'
    slither-args: '--filter-paths "test|mock|script"'
Checklist of mandatory checks before deployment
  • All external functions have access controls (onlyOwner, onlyRole)
  • Use SafeERC20 for external tokens
  • No delegatecall to unknown addresses
  • Reentrancy check in all functions with external calls
  • Presence of minAmountOut and deadline in AMM functions
  • Use of a trusted oracle (Chainlink) with deviation threshold

Audit Tools Comparison

Tool Type of Analysis What It Finds Limitations
Slither Static Reentrancy, integer overflow, access control Misses logical vulnerabilities
Mythril Symbolic execution Reachable states violating properties Path explosion on large codebases
Echidna Fuzzing (property‑based) Invariant violations Requires writing invariants
Certora Formal verification Mathematical proof of properties Doesn't work with hashes/signatures

Deliverables

  • Full report in PDF with CVSS scores for each vulnerability
  • PoC code for all Critical and High (reproducible in test environment)
  • Remediation recommendations with code examples
  • Re‑audit after fixes (up to two iterations)
  • Brief guide for developers on ongoing operation
  • Post‑deployment support for 30 days (consultations and incident analysis)

Timeline

Audit of a simple token or NFT contract — 3‑5 business days. DeFi protocol with lending/AMM — 2‑4 weeks. Full stack with multiple protocols, cross‑chain, proxy upgrades — 4‑8 weeks. Re‑audit of fixes — 3‑7 days separately.

Our team has 7+ years of experience in smart contract security, having audited over 100 projects. We guarantee we won't miss any known attack vectors — we use licensed versions of Slither and best fuzzer configurations. Assess your project — we will analyze your code for free and provide a commercial offer within 2 days. Order an audit with quality guarantee and get a discount on re‑audit for repeat customers.