Module 04 — Smart Contract Audit Methodology
Difficulty: Intermediate → Advanced
A vulnerability is only useful if you can find it consistently. This module covers the professional audit workflow used by top-tier firms like Trail of Bits, OpenZeppelin, and Spearbit. Whether you’re conducting solo audits or competing on Code4rena/Sherlock, a systematic methodology is what separates finding 2 bugs from finding 20.
4.1 Scoping
Before reading a single line of code, scope the engagement properly.
Scoping Checklist
| Dimension |
What to Assess |
How to Measure |
| Lines of code (nSLOC) |
Complexity of the codebase |
cloc --exclude-dir=test,node_modules contracts/ |
| Number of contracts |
Surface area |
Count distinct .sol files in scope |
| External dependencies |
Composability risk |
Map all import paths and external calls |
| Upgrade patterns |
Proxy complexity |
Count proxy contracts, identify upgrade mechanisms |
| Oracle dependencies |
Oracle attack surface |
List all oracle integrations |
| Token support |
Non-standard token risk |
Which ERC-20/721/1155 tokens does the protocol interact with? |
| Access control complexity |
Privileged role analysis |
Map all roles, modifiers, multisigs, timelocks |
| Previous audits |
Known issues baseline |
Read all prior audit reports |
| Test coverage |
Confidence in existing tests |
forge coverage or npx hardhat coverage |
Estimating Audit Duration
| nSLOC |
Solo Auditor |
Team (2–3) |
Competitive Audit |
| < 500 |
3–5 days |
2–3 days |
3-day contest |
| 500–2000 |
1–3 weeks |
1–2 weeks |
7-day contest |
| 2000–5000 |
3–6 weeks |
2–4 weeks |
14-day contest |
| 5000+ |
6–12 weeks |
4–8 weeks |
21+ day contest |
1
2
3
4
5
6
| # Count nSLOC using cloc (comment-aware line counter)
cloc contracts/src/ --include-lang=Solidity
# Alternative: Solidity Metrics (VS Code extension)
# Or use forge to get a quick overview
forge build --sizes
|
4.2 Manual Review Workflow
Top-Down Analysis (Feature-First)
Start from the user-facing entry points and trace execution flow.
1
2
3
4
5
6
7
8
9
10
| 1. Identify external/public functions (entry points)
2. For each entry point:
a. What state does it read?
b. What state does it modify?
c. What external calls does it make?
d. What access control protects it?
e. Can an attacker influence any input?
3. Follow the data flow through internal functions
4. Map state transitions
5. Identify invariants that should hold
|
Bottom-Up Analysis (Contract-First)
Start from the core data structures and understand the state model.
1
2
3
4
5
6
| 1. Map the storage layout (all state variables)
2. Identify critical state (balances, ownership, paused state)
3. List all functions that modify critical state
4. Verify access control on each state-modifying function
5. Check initialization of all state variables
6. Verify upgrade safety
|
Recommended Hybrid Approach
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| Phase 1 (Day 1-2): Architecture review
├── Read documentation, README, specs
├── Run Slither for initial findings
├── Map contract dependencies
└── Create a mental model of the protocol
Phase 2 (Day 3-5): Detailed code review
├── Top-down: Trace all user flows
├── Bottom-up: Verify state invariants
├── Check all external calls
└── Review access control model
Phase 3 (Day 6-7): Attack surface analysis
├── Identify economic attack vectors
├── Test edge cases with Foundry fuzzing
├── Review against vulnerability checklist
└── Cross-reference with known exploit patterns
Phase 4 (Day 8+): Exploit development & reporting
├── Write PoCs for findings
├── Classify severity
└── Draft report
|
4.3 Threat Modeling for DeFi Protocols
Trust Boundary Mapping
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| ┌──────────────────────────────────────────────────┐
│ EXTERNAL (Untrusted) │
│ ┌──────────┐ ┌──────────┐ ┌───────────────┐ │
│ │ Users │ │ Bots │ │ Flash Loans │ │
│ └──────────┘ └──────────┘ └───────────────┘ │
├──────────────────────────────────────────────────┤
│ TRUST BOUNDARY │
├──────────────────────────────────────────────────┤
│ PROTOCOL (Partially Trusted) │
│ ┌──────────┐ ┌──────────┐ ┌───────────────┐ │
│ │ Core │ │ Oracle │ │ Governance │ │
│ │ Contracts│ │ Adapter │ │ Module │ │
│ └──────────┘ └──────────┘ └───────────────┘ │
├──────────────────────────────────────────────────┤
│ TRUST BOUNDARY │
├──────────────────────────────────────────────────┤
│ ADMIN (Trusted) │
│ ┌──────────┐ ┌──────────┐ │
│ │ Multisig │ │ Timelock │ │
│ └──────────┘ └──────────┘ │
└──────────────────────────────────────────────────┘
|
Asset Flow Mapping
For every DeFi protocol, map:
- How do assets enter? (deposit functions)
- How are assets stored? (custody model — contract-held vs delegated)
- How do assets move internally? (rebalancing, strategy allocation)
- How do assets exit? (withdrawal functions, liquidation flows)
- Who controls each flow? (access control on each path)
STRIDE for Smart Contracts
| Threat |
Smart Contract Example |
| Spoofing |
tx.origin authentication, signature forgery |
| Tampering |
Storage manipulation via delegatecall, upgradeable proxy |
| Repudiation |
Missing events for critical state changes |
| Information Disclosure |
Private variables readable from storage, mempool visibility |
| Denial of Service |
Gas griefing, unbounded loops, forced ETH |
| Elevation of Privilege |
Missing access control, initialization bugs |
4.4 Control Flow & Data Flow Analysis
Control Flow Analysis
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| // Trace the control flow of a complex function
function liquidate(address borrower) external {
// 1. Validation checks
require(isHealthy(borrower) == false, "Cannot liquidate healthy position");
// 2. State reads
uint256 debt = getDebt(borrower);
uint256 collateral = getCollateral(borrower);
// 3. State modifications
_clearDebt(borrower);
// 4. External interactions
collateralToken.transfer(msg.sender, collateral); // CEI: Is this after state update? [YES]
debtToken.transferFrom(msg.sender, address(this), debt); // Is this safe?
// 5. Events
emit Liquidated(borrower, msg.sender, debt, collateral);
}
// Questions to ask at each step:
// - Can any require() be bypassed?
// - Are the reads and writes in the correct order (CEI)?
// - Can external calls reenter?
// - Is the collateral/debt calculation manipulable?
|
Data Flow Analysis
Track how user-supplied data flows through the system:
1
2
3
4
5
6
7
| User Input (calldata)
→ Function parameter
→ Used in require() check
→ Used in calculation
→ Written to storage
→ Read by another function
→ Used in external call
|
Key question at each step: Can an attacker craft input that passes validation but causes unexpected behavior downstream?
4.5 Invariant Identification & Verification
What Are Invariants?
Invariants are conditions that must always hold true, regardless of what functions are called or in what order.
Common DeFi Invariants
| Protocol Type |
Invariant |
| Token |
sum(balances) == totalSupply |
| AMM |
x * y >= k (after every swap) |
| Lending |
totalBorrowed ≤ totalDeposited |
| Vault |
shares_to_assets(total_shares) ≤ total_assets + dust |
| Staking |
sum(user_stakes) == contract_balance |
| Governance |
totalVotingPower == totalTokenSupply at snapshot |
Writing Invariant Tests in Foundry
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
| // forge test --mt invariant_
contract InvariantTest is Test {
Token token;
Handler handler;
function setUp() public {
token = new Token();
handler = new Handler(token);
// Target the handler contract for invariant testing
targetContract(address(handler));
}
// This function is called after every random sequence of handler calls
function invariant_totalSupplyMatchesBalances() public {
assertEq(token.totalSupply(), handler.ghost_totalMinted() - handler.ghost_totalBurned());
}
function invariant_noUserExceedsTotalSupply() public {
for (uint i = 0; i < handler.actorCount(); i++) {
assertLe(token.balanceOf(handler.actors(i)), token.totalSupply());
}
}
}
contract Handler is Test {
Token token;
uint256 public ghost_totalMinted;
uint256 public ghost_totalBurned;
function deposit(uint256 amount) external {
amount = bound(amount, 1, 1e24);
token.mint(msg.sender, amount);
ghost_totalMinted += amount;
}
function withdraw(uint256 amount) external {
amount = bound(amount, 0, token.balanceOf(msg.sender));
token.burn(msg.sender, amount);
ghost_totalBurned += amount;
}
}
|
4.6 State Machine Analysis
Modeling Contract State
Many contracts implement implicit state machines. Making these explicit helps find invalid state transitions.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| ┌─────────┐ deposit() ┌──────────┐
│ IDLE │──────────────────→│ ACTIVE │
└─────────┘ └──────────┘
│
│ liquidate() │
│ ┌──────────────────┘
│
┌─────────┐ ┌──────────────┐
│ IDLE │←──│ LIQUIDATING │
└─────────┘ └──────────────┘
Questions:
- Can you call deposit() while LIQUIDATING? (Should be blocked)
- Can you call withdraw() while ACTIVE? (Should be allowed)
- Can the state skip from IDLE to LIQUIDATING? (Should not happen)
- What happens if two state transitions are called in the same tx?
|
4.7 Economic Attack Surface Modeling
Categories of Economic Attacks
| Category |
Description |
Example |
| Price manipulation |
Changing the price an oracle reports |
Flash loan → dumps token → oracle reads low price |
| Liquidity manipulation |
Adding/removing liquidity to affect operations |
JIT liquidity in Uniswap V3 |
| Collateral manipulation |
Artificially inflating collateral value |
Donating tokens to inflate share price |
| Governance manipulation |
Acquiring voting power temporarily |
Flash loan governance (Beanstalk) |
| Interest rate manipulation |
Changing borrow/supply rates profitably |
Large deposit to reduce rates, then borrow cheaply |
Analysis Framework
For each economic interaction:
- Who profits? Follow the money
- Who loses? Are losses socialized (shared by all depositors)?
- What is the capital requirement? (Flash loans make it effectively zero)
- Is the profit extractable atomically? (Single transaction = flash-loan-able)
- What parameters can an attacker control? (Amounts, timing, order)
4.8 Integration Risk Analysis (Composability Attacks)
DeFi’s biggest risk is composability — protocols built on top of other protocols inherit all downstream risks.
Integration Audit Checklist
4.9 Test Coverage Analysis
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| # Foundry coverage
forge coverage
# Foundry coverage with specific report format
forge coverage --report lcov
# Then view in VS Code with Coverage Gutters extension
# Hardhat coverage
npx hardhat coverage
# What to look for:
# - Lines not covered by tests → untested edge cases
# - Branches not covered → conditional logic untested
# - Functions not covered → entire code paths untested
|
Coverage Is Necessary But Not Sufficient
100% line coverage does NOT mean the contract is secure. Coverage tells you which code executed, not whether the assertions are correct. A test that calls every function but never checks the output gives 100% coverage with 0% assurance.
4.10 Checklist-Driven Audit (SWC Registry)
SWC Registry Quick Reference
| SWC ID |
Name |
Severity |
| SWC-100 |
Function Default Visibility |
Medium |
| SWC-101 |
Integer Overflow/Underflow |
High |
| SWC-104 |
Unchecked Return Value |
High |
| SWC-106 |
Unprotected SELFDESTRUCT |
Critical |
| SWC-107 |
Reentrancy |
High-Critical |
| SWC-110 |
Assert Violation |
Medium |
| SWC-111 |
Use of Deprecated Functions |
Low |
| SWC-112 |
Delegatecall to Untrusted Callee |
Critical |
| SWC-113 |
DoS with Failed Call |
Medium |
| SWC-114 |
Transaction Order Dependence |
Medium |
| SWC-115 |
Authorization through tx.origin |
High |
| SWC-116 |
Block Timestamp Dependence |
Low |
| SWC-120 |
Weak Randomness |
Medium |
| SWC-123 |
Requirement Violation |
Medium |
| SWC-124 |
Write to Arbitrary Storage |
Critical |
| SWC-128 |
DoS with Block Gas Limit |
Medium |
| SWC-131 |
Presence of Unused Variables |
Low |
| SWC-136 |
Unencrypted Private Data |
Medium |
Full registry: swcregistry.io
4.11 Writing Professional Audit Findings
Finding Template
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| ## [H-01] Title describing the vulnerability concisely
### Severity
**High** / **Critical** / **Medium** / **Low** / **Informational**
### Description
Technical description of the vulnerability. What is wrong, where is it, and why does it exist?
### Impact
What happens if this is exploited? Quantify the damage:
- Funds at risk: $X locked in the contract
- Affected users: all depositors / specific role
- Exploitability: requires flash loan / requires admin key / anyone can exploit
### Proof of Concept
```solidity
// Foundry test demonstrating the exploit
function test_exploit() public {
// Setup
// Action
// Assertion showing the vulnerability
}
\```
### Recommended Mitigation
```solidity
// Specific code change with before/after
// BEFORE (vulnerable):
function withdraw() external { ... }
// AFTER (fixed):
function withdraw() external nonReentrant { ... }
\```
### Likelihood
- Attack complexity: Low / Medium / High
- Prerequisites: None / Flash loan / Admin key
### References
- Similar exploit: [Rekt News link]
- SWC Registry: SWC-107
|
Severity Classification
| Severity |
Impact |
Likelihood |
Examples |
| Critical |
Direct loss of funds, protocol takeover |
High (easy to exploit) |
Reentrancy draining all funds, unprotected admin functions |
| High |
Significant fund loss, major protocol disruption |
Medium-High |
Oracle manipulation, governance attack, signature replay |
| Medium |
Limited fund loss, protocol degradation |
Medium |
Precision loss giving small advantage, DoS vectors |
| Low |
No direct fund loss, minor issues |
Low |
Missing events, gas optimizations, informational |
4.12 Audit Report Structure
Professional Report Structure
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| 1. Executive Summary
├── Scope (contracts, commit hash, nSLOC)
├── Methodology overview
├── Summary of findings (Critical: X, High: Y, Medium: Z, Low: W)
└── Overall risk assessment
2. Table of Contents
3. Scope & Methodology
├── Contracts in scope (with addresses if deployed)
├── Lines of code
├── Tools used
├── Review timeline
└── Audit team
4. System Overview
├── Architecture diagram
├── Key contracts and their roles
├── Trust model
└── External dependencies
5. Findings
├── Critical
├── High
├── Medium
├── Low
└── Informational
6. Appendix
├── Tool output (Slither, Mythril)
├── Test coverage report
└── Disclaimer
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| #!/bin/bash
# audit-pipeline.sh — Run before manual review
echo "=== Step 1: Compilation ==="
forge build
echo "=== Step 2: Slither (Static Analysis) ==="
slither . --json slither-output.json
slither . --print human-summary
echo "=== Step 3: Slither Detectors (Specific) ==="
slither . --detect reentrancy-eth,reentrancy-no-eth,unchecked-lowlevel,arbitrary-send-eth,\
controlled-delegatecall,suicidal,uninitialized-storage,unprotected-upgrade
echo "=== Step 4: Aderyn (Fast AST Analysis) ==="
aderyn .
echo "=== Step 5: Test Coverage ==="
forge coverage --report summary
echo "=== Step 6: Storage Layout ==="
forge inspect MainContract storage-layout
echo "=== Step 7: Contract Size Check ==="
forge build --sizes
echo "=== Step 8: Gas Report ==="
forge test --gas-report
|
Triage Process for Automated Findings
1
2
3
4
5
| Automated Finding → Categorize:
├── True Positive (confirmed vulnerability) → Write up as finding
├── True Positive (known/accepted risk) → Note in informational
├── False Positive → Dismiss with reason
└── Needs Investigation → Manual review required
|
Key Takeaway: Tools find ~20–30% of real vulnerabilities. The remaining 70–80% — logic errors, economic exploits, cross-contract interactions — require manual analysis. Use tools as an initial pass to catch low-hanging fruit, then spend the majority of your time on manual review of business logic and economic invariants.