How to Audit a DeFi Smart Contract: A Step-by-Step Guide
Between 2021 and 2025, DeFi exploits drained over $6 billion from smart contracts. Euler Finance lost $197 million to a faulty donation mechanism. Beanstalk lost $182 million to a governance flash loan attack. Nomad Bridge lost $190 million because a routine upgrade accidentally marked every message as valid. These were not obscure protocols — they were established projects with audits, large TVLs, and experienced teams.
Every one of those exploits was preventable. The vulnerabilities were hiding in plain sight: unchecked external calls, missing access controls, flawed economic assumptions. A rigorous smart contract audit following a systematic methodology would have caught them.
This guide walks through the exact process we use to audit DeFi protocols. Whether you are a developer preparing for your first audit, a security researcher sharpening your methodology, or a protocol team evaluating auditor quality, this step-by-step checklist will give you a concrete framework for a thorough DeFi security audit.
The 10-Step Smart Contract Audit Checklist
A smart contract audit is not a single pass through the code. It is a structured, multi-phase process that combines manual review with automated tooling, economic analysis with code-level inspection. Here is the complete solidity audit checklist we follow on every engagement.
Understand the protocol's design before reading a single line of code. Review documentation, architecture diagrams, and the intended threat model.
Map all external dependencies: OpenZeppelin versions, oracle integrations, token standards, proxy patterns. Check for known vulnerabilities in pinned library versions.
Identify every privileged function. Who can pause? Who can upgrade? Who can change fees? Map the trust assumptions and check for missing modifiers.
Trace every external call. Verify checks-effects-interactions ordering. Look for cross-function and cross-contract reentrancy paths.
Check for overflow/underflow (even with Solidity 0.8+, unchecked blocks reintroduce the risk), rounding errors in share calculations, and precision loss in token conversions.
Verify oracle freshness checks, fallback mechanisms, and manipulation resistance. Check if spot prices from AMMs are used where TWAPs should be.
Model flash loan attack paths, MEV extraction, sandwich attacks, and governance manipulation. This is where the largest DeFi exploits live.
Run Slither, Mythril, and custom pattern detectors. Automated tools catch low-hanging fruit and surface code paths a manual reviewer might miss.
For every potential finding, write a Foundry or Hardhat test that demonstrates the exploit. If you cannot prove it, it is not a finding — it is a hypothesis.
Document findings with severity, impact, and recommended fixes. After the team remediates, verify that every fix is correct and does not introduce new issues.
Now let us walk through each step in detail, with real-world examples and code patterns to watch for.
Step 1: Scope Definition and Architecture Review
Before opening a single Solidity file, you need to understand what the protocol is supposed to do. Read the whitepaper. Study the documentation. Draw out the contract interaction flow. The goal is to build a mental model of the system so that when you read the code, you can identify deviations from the intended design.
Key questions at this stage:
- What assets does the protocol hold, and what are the flows in and out?
- Which contracts are upgradeable, and who controls the upgrades?
- What external protocols does the system integrate with (oracles, DEXs, bridges)?
- What are the protocol's stated invariants — conditions that must always be true?
Step 2: Dependency and Import Analysis
Modern DeFi protocols inherit from dozens of libraries. Each dependency is attack surface. Check the exact version of every imported library. Look for known CVEs in OpenZeppelin releases. Verify that proxy implementations match their intended patterns (UUPS vs. Transparent vs. Beacon). A single version mismatch can introduce a critical vulnerability.
Pay special attention to:
- OpenZeppelin contract versions — several releases had initialization bugs in upgradeable contracts
- Solmate vs. OpenZeppelin ERC-20 implementations (different rounding behavior in share-based vaults)
- Oracle wrapper contracts that may mask staleness or return stale prices silently
Step 3: Access Control and Privilege Review
Access control bugs are the most common high-severity finding category in smart contract audits. The pattern is almost always the same: a function that should be restricted to the owner, a specific contract, or a governance timelock is left unprotected.
// VULNERABLE: Missing access control on critical function
contract VulnerableVault {
mapping(address => uint256) public balances;
// Anyone can call this and drain any user's balance
function withdraw(address user, uint256 amount) external {
balances[user] -= amount;
payable(msg.sender).transfer(amount);
}
}
// FIXED: Proper access control
contract SecureVault {
mapping(address => uint256) public balances;
// Only the user themselves can withdraw their balance
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount, "Insufficient");
balances[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
}
}
Map every onlyOwner, onlyRole, and custom modifier in the codebase. For each privileged function, ask: what is the worst thing that can happen if this is called by an attacker? If the answer involves fund loss, double-check the access control mechanism.
Step 4: Reentrancy and External Call Analysis
Reentrancy remains a top vulnerability class despite being well understood since The DAO hack in 2016. The pattern has evolved: modern reentrancy exploits often involve cross-function or cross-contract reentrancy, where the state inconsistency is exploited through a different entry point than the one making the external call.
// VULNERABLE: Classic reentrancy — state updated after external call
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount);
(bool ok, ) = msg.sender.call{value: amount}(""); // External call BEFORE state update
require(ok);
balances[msg.sender] -= amount; // Too late — attacker re-entered
}
// FIXED: Checks-Effects-Interactions pattern
function withdraw(uint256 amount) external nonReentrant {
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount; // State updated FIRST
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok);
}
When auditing for reentrancy, trace every .call(), .transfer(), .send(), and ERC-777 token transfer. Then check: is there any state that has been read but not yet updated at the point of the external call? If yes, can another function in the protocol read that stale state?
Step 5: Arithmetic and Precision Review
Solidity 0.8+ introduced automatic overflow checks, but that does not eliminate arithmetic vulnerabilities. The real dangers in DeFi are rounding errors in share-based accounting and precision loss in token conversions, especially between tokens with different decimal counts.
// VULNERABLE: First depositor inflation attack on ERC-4626 vault
// Attacker deposits 1 wei, then donates 1e18 tokens directly
// Next depositor's shares round down to 0
function convertToShares(uint256 assets) public view returns (uint256) {
uint256 supply = totalSupply();
return supply == 0 ? assets : assets * supply / totalAssets();
// If totalAssets = 1e18+1 and supply = 1,
// depositing 999e15 tokens gives: 999e15 * 1 / (1e18+1) = 0 shares
}
// FIXED: Virtual shares and assets offset (OpenZeppelin approach)
function convertToShares(uint256 assets) public view returns (uint256) {
return assets.mulDiv(totalSupply() + 1e3, totalAssets() + 1, Math.Rounding.Floor);
// Virtual offset of 1e3 makes inflation attack economically infeasible
}
The first-depositor inflation attack has affected multiple ERC-4626 vaults in production. It is a mandatory check on every vault audit.
Step 6: Oracle and Price Feed Validation
Oracle manipulation is the root cause of some of the largest DeFi exploits ever. Any protocol that uses external price data must validate freshness, handle failure gracefully, and resist manipulation.
When reviewing oracle usage, verify these critical checks:
// VULNERABLE: No staleness check, no price validation
function getPrice() external view returns (uint256) {
(, int256 price,,,) = priceFeed.latestRoundData();
return uint256(price); // Could be stale, zero, or negative
}
// FIXED: Proper oracle validation
function getPrice() external view returns (uint256) {
(
uint80 roundId,
int256 price,
,
uint256 updatedAt,
uint80 answeredInRound
) = priceFeed.latestRoundData();
require(price > 0, "Invalid price");
require(updatedAt > block.timestamp - STALENESS_THRESHOLD, "Stale price");
require(answeredInRound >= roundId, "Incomplete round");
return uint256(price);
}
Missing staleness checks on Chainlink feeds is one of the most common medium-severity findings in DeFi audits. It is easy to fix but easy to overlook — and exploitable when the Chainlink feed goes down or lags during volatile markets.
Step 7: Economic and Game Theory Analysis
This is the step that separates amateur audits from professional ones. Code-level bugs are necessary to find, but the highest-impact DeFi exploits are often economic design flaws rather than Solidity bugs.
Economic analysis should model:
- Flash loan attack paths: Can an attacker borrow enough capital to manipulate prices, voting, or liquidation thresholds within a single transaction?
- MEV extraction: Can validators or searchers extract value by reordering, inserting, or censoring transactions?
- Sandwich attacks: Are there large swaps or deposits that can be profitably sandwiched?
- Incentive misalignment: Does the protocol assume rational behavior that a sufficiently capitalized attacker could violate?
Step 8: Automated Tool Scanning
Automated tools are not a substitute for manual review, but they are an essential complement. They excel at finding patterns that humans miss through fatigue — unchecked return values, unused variables, gas optimizations that accidentally change behavior.
The standard toolkit for a professional smart contract audit:
| Tool | Strength | Limitations |
|---|---|---|
| Slither | Fast static analysis, detects 80+ vulnerability patterns | High false positive rate on complex patterns |
| Mythril | Symbolic execution finds deep bugs | Slow on large codebases, may timeout |
| Foundry Fuzzing | Property-based testing with coverage guidance | Requires writing invariant tests |
| Echidna | Smart contract fuzzer with corpus-based coverage | Configuration-heavy, learning curve |
Run every tool in your pipeline. Review every finding. But treat automated results as leads, not conclusions. The real audit work is in understanding why a tool flagged something and whether it represents a genuine risk in the protocol's specific context.
Step 9: Proof-of-Concept Development
A finding without a proof of concept is an opinion. A finding with a working exploit is a fact. For every potential vulnerability, write a Foundry test that demonstrates the attack from start to finish: the attacker's initial state, the sequence of calls, and the final state showing the impact.
// Example: Foundry PoC for a reentrancy exploit
function testReentrancyExploit() public {
// Setup: attacker deposits 1 ETH into vulnerable vault
vm.deal(attacker, 1 ether);
vm.prank(attacker);
vault.deposit{value: 1 ether}();
// Record balances before attack
uint256 vaultBefore = address(vault).balance; // 10 ETH (other deposits)
// Execute attack
vm.prank(attacker);
attackContract.exploit();
// Verify: attacker drained the vault
assertEq(address(vault).balance, 0);
assertGe(address(attackContract).balance, vaultBefore);
}
PoC development also serves as a sanity check on your own analysis. Many seemingly critical findings evaporate when you actually try to exploit them — gas limits, transaction ordering constraints, or economic infeasibility can all prevent a theoretical vulnerability from being practically exploitable.
Step 10: Report, Remediation, and Re-Audit
The final audit report must be actionable. For each finding, include:
- Severity rating — Critical, High, Medium, Low, or Informational with clear justification
- Affected code — Exact file, function, and line numbers
- Description — What the vulnerability is and why it exists
- Impact — What an attacker can achieve and the maximum potential loss
- Proof of concept — A working Foundry test demonstrating the exploit
- Recommended fix — Specific code changes, not vague suggestions
After the team implements fixes, a re-audit is mandatory. Remediations frequently introduce new bugs — a fix for a reentrancy issue might add a nonReentrant modifier to the wrong function, or a new access control check might lock out the protocol's own contracts. Verify every fix independently.
Common Vulnerability Patterns: Quick Reference
| Pattern | Example Exploit | Loss |
|---|---|---|
| Oracle manipulation | Euler Finance — donation-based exchange rate manipulation | $197M |
| Flash loan governance | Beanstalk — flash-borrowed voting power | $182M |
| Initialization bug | Nomad Bridge — trusted root set to 0x00 | $190M |
| Reentrancy | The DAO — recursive withdrawal | $50M |
| Access control | Parity Wallet — unprotected initWallet | $30M |
| First depositor inflation | Multiple ERC-4626 vaults | Various |
| Unchecked return value | Failed transfer() calls silently ignored | Various |
How Long Does a Smart Contract Audit Take?
Timelines vary by codebase complexity, but here are realistic estimates based on our experience:
| Scope | Lines of Code | Timeline |
|---|---|---|
| Single contract (token, vault) | 200-500 LoC | 2-5 days |
| Small protocol (DEX, lending pool) | 1,000-3,000 LoC | 1-2 weeks |
| Medium protocol (multi-chain, complex DeFi) | 3,000-10,000 LoC | 2-4 weeks |
| Large protocol (full ecosystem audit) | 10,000+ LoC | 4-8 weeks |
Rushing an audit is worse than skipping one. A rushed audit gives a false sense of security — the team believes they have been audited, but critical paths were not examined. If your timeline does not allow for a thorough review, narrow the scope to the highest-risk contracts rather than doing a shallow pass over everything.
Before You Deploy: Pre-Audit Preparation
If you are a protocol team preparing for an audit, the quality of your preparation directly affects the quality of findings your auditor can deliver. Before engaging an auditor:
- Document your invariants. Write down every condition that must always be true. "Total deposits must always equal total shares times the exchange rate." Auditors will try to break these.
- Write comprehensive tests. Aim for 90%+ line coverage. Untested code paths are where bugs hide.
- Run Slither yourself. Fix the low-hanging issues before paying an auditor to find them.
- Freeze the codebase. Do not make changes during the audit. Every change invalidates findings and wastes auditor time.
- Prepare a threat model. Document what you are most worried about. The auditor will prioritize those areas.
Get Your Smart Contract Scanned in Seconds
Our automated scanner detects 20+ vulnerability patterns including reentrancy, access control flaws, oracle manipulation risks, and arithmetic issues. Paste your contract address or source code and get results instantly.
Launch Full Scanner Try Free ScanFree scan covers 8 critical patterns. Full scanner covers 20+ patterns with detailed remediation guidance.