Caloogy Article
中文

What Smart Contract Auditing and Formal Verification Do

Date
Jun 16, 2026
Series
Onchain
Reading
4 min
Topics
OnchainSmart contractAuditFormal verification

The one-sentence version

Once a smart contract is deployed, it is difficult to modify, and since it directly manages money, pre-launch inspection is extremely important. Common methods range from light to heavy: Static Analysis (automatically finding common error patterns), Fuzzing and Invariant Testing (randomly generating large sequences of calls to check if certain properties always hold), Manual Audit (experts reading line by line), and Formal Verification (using mathematics to prove that code satisfies specifications). However, "audited" does not mean "secure": an audit only covers a given version and a given scope, and auditors can miss things. Understanding what each method can and cannot guarantee is essential to correctly interpreting an audit report.

This article has three layers:

  1. Layer 1: The Intuition: Physical exams, stress tests, and mathematical proofs
  2. Layer 2: Real-World Use: Mainstream tools, why "audited" projects are still hacked, and how to read audit reports
  3. Layer 3: Invariants, formal verification boundaries, and engineering details

Layer 1: The Intuition

1.1 Four Methods of Inspection

Method Analogy Strengths Weaknesses
Static Analysis Routine items in a physical exam Quickly finding known error patterns Business logic errors
Fuzzing / Invariant Testing Stress testing: Thousands of random operations Finding call sequences humans haven't thought of Requires defining "what constitutes an error" first
Manual Audit Expert consultation Business logic, economic models, design flaws Limited by time and human attention
Formal Verification Mathematical proof Exhausting all cases within the specification If the specification itself is wrong, the proof is useless

1.2 "What is correct" is the hardest part

All methods require answering one question: what properties should this contract satisfy? For example:

Defining these properties clearly is often more valuable than finding bugs themselves.


Layer 2: Real-World Use

2.1 Mainstream Tools

Tool Type
Slither Static analysis, detecting hundreds of patterns like reentrancy, uninitialized variables, missing permissions, etc.
Foundry (fuzz / invariant) Fuzzing and invariant testing, write tests directly in Solidity
Echidna, Medusa Property-based fuzzers
Certora, Halmos, SMTChecker Formal verification / Symbolic execution
Mainnet Fork Testing Replicating the mainnet state locally to test interactions with real protocols (see "Flash Loans and Price Manipulation")

2.2 Why "Audited" Projects are Still Hacked

Reason Description
Out of Scope Only the core contracts were audited, not the later added modules, frontend, or deployment scripts
Code changed after audit The audit was for v1, but v1.1 (with "a few small fixes") was deployed
Composability Risk The contract itself is fine, but issues arise when combined with other protocols (e.g., read-only reentrancy, price oracles)
Economic Attack The code runs as designed, but the design itself can be exploited under extreme market conditions
Operations & Keys The contract is fine, but the administrator's private key was stolen (see "Upgradable Contracts and Admin Private Keys")
Toolchain Compiler bugs (see the Curve incident in "Reentrancy Attacks")
Auditor Oversight Time is limited, and humans get fatigued

2.3 How to Read an Audit Report


Layer 3: Principles and Engineering

3.1 Invariant Testing

An invariant is a property that "must always hold true, regardless of how users call the contract or in what order they call it." A testing framework will randomly generate a large number of call sequences and check the invariant after every step:

// Invariant: actual tokens held by contract ≥ sum of all recorded user balances
function invariant_solvent() public {
    assertGe(token.balanceOf(address(vault)), vault.totalDeposits());
}

Good invariants come from an understanding of the business logic:

Category Example
Solvency Assets ≥\ge Liabilities
Conservation Total supply = Sum of all balances
Permissions Any call to an administrative function by a non-owner must fail
Monotonicity A certain counter only increases; a certain price does not change by more than a certain ratio within a single transaction
No Arbitrage Any "deposit then immediately withdraw" sequence should not allow the caller to profit

3.2 Formal Verification

The contract code and properties are translated into mathematical logic, and a solver is used to prove that "for all possible inputs and states, the property holds"; if it does not hold, the solver provides a specific counter-example.

It is stronger than testing because it is exhaustive: testing can only try a finite number of cases, while proof covers everything. However, it has clear boundaries:

3.3 A Recommended Workflow

  1. Clearly write the specification: what the contract should and should not do, and what its invariants are;
  2. Run continuous static analysis and unit tests during development;
  3. Write invariant tests and run fuzzing for a long duration;
  4. Perform formal verification on core logic (if conditions allow);
  5. Multiple independent audits;
  6. Post-deployment: Bug bounties, real-time monitoring, pause switches, and limits (see "Fail-closed").

Each step can only reduce risk, not eliminate it. "Defense in depth" means: problems missed by the earlier layers have a chance to be caught or their losses limited by the later layers.


Summary

Layer Key points
Four Methods Static analysis, Fuzzing / Invariant testing, Manual audit, Formal verification
Core Challenge First define "what is correct": write clear specifications and invariants
Audit Limitations Scope, version, composability risk, economic attacks, operations
Practice Layered approach; post-deployment requires bounties, monitoring, and circuit breakers