Solari Systems — Smart Contract Security
GUIDE DEFI SECURITY SOLIDITY

How to Audit a DeFi Smart Contract: A Step-by-Step Guide

March 11, 2026 • 12 min read • Solari Systems Research

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.

Who this is for: Solidity developers, security researchers, protocol teams, and anyone who wants to understand what a professional smart contract audit actually involves. We assume familiarity with Solidity basics and DeFi primitives (AMMs, lending, oracles).

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.

1
Scope Definition and Architecture Review
Understand the protocol's design before reading a single line of code. Review documentation, architecture diagrams, and the intended threat model.
2
Dependency and Import Analysis
Map all external dependencies: OpenZeppelin versions, oracle integrations, token standards, proxy patterns. Check for known vulnerabilities in pinned library versions.
3
Access Control and Privilege Review
Identify every privileged function. Who can pause? Who can upgrade? Who can change fees? Map the trust assumptions and check for missing modifiers.
4
Reentrancy and External Call Analysis
Trace every external call. Verify checks-effects-interactions ordering. Look for cross-function and cross-contract reentrancy paths.
5
Arithmetic and Precision Review
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.
6
Oracle and Price Feed Validation
Verify oracle freshness checks, fallback mechanisms, and manipulation resistance. Check if spot prices from AMMs are used where TWAPs should be.
7
Economic and Game Theory Analysis
Model flash loan attack paths, MEV extraction, sandwich attacks, and governance manipulation. This is where the largest DeFi exploits live.
8
Automated Tool Scanning
Run Slither, Mythril, and custom pattern detectors. Automated tools catch low-hanging fruit and surface code paths a manual reviewer might miss.
9
Proof-of-Concept Development
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.
10
Report, Remediation, and Re-Audit
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:

Real-world lesson — Nomad Bridge ($190M): In August 2022, a routine upgrade to Nomad Bridge accidentally initialized the trusted root to 0x00. This meant every message was automatically considered valid. An architecture-level review would have flagged that the upgrade changed the validation root — the single most critical security parameter in the entire system. Hundreds of attackers copied the first exploit transaction and drained $190 million before the contracts could be paused.

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:

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.

Real-world lesson — Euler Finance ($197M): In March 2023, Euler Finance was exploited for $197 million. The attacker used a flash loan to manipulate Euler's internal accounting through a flawed donation mechanism in the donateToReserves function. The protocol allowed users to donate funds to the reserve, which inflated the exchange rate of eTokens. Combined with leveraged borrowing, this let the attacker create an undercollateralized position and drain the lending pool. A thorough economic analysis of the donation function's interaction with the leverage system would have flagged this path.

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.

Real-world lesson — Beanstalk ($182M): In April 2022, an attacker used a flash loan to accumulate enough Stalk (Beanstalk's governance token) to single-handedly pass a malicious governance proposal in a single transaction. The proposal drained the protocol's treasury. The Solidity code was correct — the governance mechanism worked exactly as designed. The flaw was economic: governance voting power was based on a token that could be acquired instantly via flash loan. A game theory analysis asking "what if an attacker can borrow unlimited governance tokens for one block?" would have identified this vulnerability immediately.

Economic analysis should model:

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:

ToolStrengthLimitations
SlitherFast static analysis, detects 80+ vulnerability patternsHigh false positive rate on complex patterns
MythrilSymbolic execution finds deep bugsSlow on large codebases, may timeout
Foundry FuzzingProperty-based testing with coverage guidanceRequires writing invariant tests
EchidnaSmart contract fuzzer with corpus-based coverageConfiguration-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:

  1. Severity rating — Critical, High, Medium, Low, or Informational with clear justification
  2. Affected code — Exact file, function, and line numbers
  3. Description — What the vulnerability is and why it exists
  4. Impact — What an attacker can achieve and the maximum potential loss
  5. Proof of concept — A working Foundry test demonstrating the exploit
  6. 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

PatternExample ExploitLoss
Oracle manipulationEuler Finance — donation-based exchange rate manipulation$197M
Flash loan governanceBeanstalk — flash-borrowed voting power$182M
Initialization bugNomad Bridge — trusted root set to 0x00$190M
ReentrancyThe DAO — recursive withdrawal$50M
Access controlParity Wallet — unprotected initWallet$30M
First depositor inflationMultiple ERC-4626 vaultsVarious
Unchecked return valueFailed transfer() calls silently ignoredVarious

How Long Does a Smart Contract Audit Take?

Timelines vary by codebase complexity, but here are realistic estimates based on our experience:

ScopeLines of CodeTimeline
Single contract (token, vault)200-500 LoC2-5 days
Small protocol (DEX, lending pool)1,000-3,000 LoC1-2 weeks
Medium protocol (multi-chain, complex DeFi)3,000-10,000 LoC2-4 weeks
Large protocol (full ecosystem audit)10,000+ LoC4-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.

Key takeaway: A smart contract audit is not a checkbox exercise. It is a structured adversarial review that combines code analysis, economic modeling, automated tooling, and hands-on exploit development. The protocols that survive are the ones that treat security as a continuous process, not a one-time event before launch.

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:

  1. 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.
  2. Write comprehensive tests. Aim for 90%+ line coverage. Untested code paths are where bugs hide.
  3. Run Slither yourself. Fix the low-hanging issues before paying an auditor to find them.
  4. Freeze the codebase. Do not make changes during the audit. Every change invalidates findings and wastes auditor time.
  5. 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 Scan

Free scan covers 8 critical patterns. Full scanner covers 20+ patterns with detailed remediation guidance.