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:
- Layer 1: The Intuition: Physical exams, stress tests, and mathematical proofs
- Layer 2: Real-World Use: Mainstream tools, why "audited" projects are still hacked, and how to read audit reports
- 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:
- "The sum of all user deposits must never exceed the actual tokens held by the contract";
- "Only the owner can call the upgrade function";
- "The amount of money any person withdraws cannot exceed their deposit plus earned interest."
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
- Check the Scope: Which files and which commit hash were audited? Does it match the bytecode deployed on-chain?
- Check the Findings: How many High and Medium severity issues were found? Is the remediation status "Fixed", "Acknowledged but not fixed", or "Disputed"?
- Check the Auditor: Are they independent? Are there multiple audits for the same project? Is there a bug bounty program (see "Responsible Disclosure and Bug Bounties")?
- Check the Timing: How long ago was the audit? Has the code been upgraded since then?
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 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:
- Only proves what you write: If you miss an important property, the proof will still "pass";
- Model assumptions: For example, assuming external contracts behave normally, whereas in the real world, an external contract might be written by an attacker;
- High cost: Requires specialized talent, and complex contracts may fail to prove (timeout).
3.3 A Recommended Workflow
- Clearly write the specification: what the contract should and should not do, and what its invariants are;
- Run continuous static analysis and unit tests during development;
- Write invariant tests and run fuzzing for a long duration;
- Perform formal verification on core logic (if conditions allow);
- Multiple independent audits;
- 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 |