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
-
totalSupplynever exceedsMAX_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.







