Zakaria Eddafri
Bug Bounty Hunter

At HackenProof, we believe that some of the most valuable security knowledge is created inside the hacker community itself. This belief is reflected in our ongoing series of guest articles, where security researchers from our community share practical insights, practical knowledge, and real-world lessons from their work in smart contract security, Web3 development, and bug bounty research. By publishing hacker-authored content, we aim to make expert-level security knowledge more accessible and to support continuous learning across the broader Web3 security ecosystem.

We regularly curate and publish the strongest technical articles based on their educational value, technical depth, and relevance to real security challenges. Authors whose work is published on the HackenProof blog receive the Star Author achievement, recognizing their contribution to knowledge sharing and community growth.

Read the article, explore the ideas, and share your thoughts with the community — and if you have expertise to share, this could be your first step toward becoming our next Star Author.

Background and Scope

This article was written by Zakaria Eddafri (@GallopingMrOwl) – an ethical hacker who began his journey with nothing more than an Android phone. His path on HackenProof started since October 1, 2019. Now he has 54 paid reports and this is his second article on our blog.

This article introduces a structured, battle-tested 10-phase audit methodology that helps security researchers move from line-by-line reading to systematic threat modeling. It walks through reconnaissance, grep strategy, static and dynamic analysis, architecture mapping, fuzzing, and exploit PoC development across Solidity, Rust (Solana), Move, and Cosmos. Instead of teaching how to “read” smart contracts, it teaches how to hunt them.

🖊️ Editor’s note: The following section contains the original article written by the author and is published as submitted, with minimal editorial changes.

Foreword

There’s a moment every auditor knows well. You open a repository, see forty Solidity files staring back at you, and the first instinct is to start reading from the top of Contract.sol. Three hours later you’re drowning in implementation details, holding a mental model of three functions but no coherent picture of the system.

This isn’t a knowledge problem. It’s a methodology problem.

The way most people read smart contracts — linearly, diving straight into logic — is fundamentally wrong for security work. Auditing isn’t reading. It’s building a mental model of a system, then probing that model for failure modes. The order in which you gather information determines the quality of that model.

Here’s the framework I use for any project, regardless of size or complexity.

Before You Touch a Single File

Ask three questions before opening anything:

What is this project? A DEX, a lending protocol, an NFT marketplace, a staking system? The category tells you the threat model before you’ve read a line.

What problem is it solving? Not the marketing answer — the mechanical one. “Users deposit tokens and earn yield” is a real answer. “Decentralized finance” is not.

Who are the actors? Identify every entity that interacts with the system: regular users, liquidity providers, admins, external protocols, governance mechanisms. Each actor is a potential attacker.

You can usually answer all three from the README, the project name, or the first few comments in the main contract. This step takes five minutes and shapes everything that follows.

# Start with the README if it exists
cat README.md

# Or check the first comments in the main contract
head -50 contracts/MainContract.sol

Phase 1: Reconnaissance — Map the Territory

Before reading any logic, understand what you’re working with. This phase typically takes 2–4 hours and sets the quality of everything that follows.

# Get a full picture of the project layout
ls -la

# Count total files by type — sets expectations for scope
find . -name "*.sol" | wc -l
find . -name "*.rs"  | wc -l
find . -name "*.move" | wc -l

# Count lines of code across the project
find . -name "*.sol" -type f -exec wc -l {} \\\\;
cloc .

# See the folder structure at a glance
find . -type d

# Build the project to verify it compiles cleanly
forge build
# or: cargo build / aptos move compile

Most protocols follow recognizable folder patterns: interfaces/ is the public API surface (start here), core/ or contracts/ holds the main logic, libs/ or libraries/ contains helper and math functions, utils/ has peripheral utilities, and test/ is an underutilized gold mine — tests are executable documentation.

Grep for Entry Points

Once you have a layout, grep before you read. This gives you the full map without committing to any specific file yet.

# All public and external functions — every entry point
grep -RIn "function " contracts/ | tee all_functions.txt
grep -r "function .*public" src | wc -l && grep -r "function .*external" src | wc -l
grep -RIn "external\\\\|public" contracts/

# Access control
grep -RIn "onlyOwner\\\\|onlyAdmin\\\\|require(msg.sender" contracts/
grep -RIn "modifier" contracts/
grep -RIn "private\\\\|internal" contracts/

# Events (what state transitions are announced?)
grep -RIn "event " contracts/
grep -RIn "require(" contracts/

Grep for Money Flow (Critical)

# ETH transfers
grep -RIn "transfer\\\\|send\\\\|call{" contracts/

# Token transfers
grep -RIn "transferFrom\\\\|safeTransfer" contracts/

# Approval patterns
grep -RIn "approve\\\\|allowance" contracts/

# State-changing money operations
grep -RIn "withdraw\\\\|deposit\\\\|mint\\\\|burn" contracts/

Grep for Dangerous Patterns

# Proxy and delegate patterns
grep -RIn "delegatecall\\\\|staticcall" contracts/

# Contract destruction
grep -RIn "selfdestruct\\\\|suicide" contracts/

# Inline assembly (anything can happen here)
grep -RIn "assembly\\\\|mstore\\\\|mload" contracts/

# Encoding — watch for abi.encodePacked collision vulnerabilities
grep -RIn "abi.encode\\\\|abi.decode\\\\|abi.encodePacked" contracts/

# Phishing vulnerability
grep -RIn "tx.origin" contracts/

# Time manipulation vectors
grep -RIn "block.timestamp\\\\|block.number" contracts/

Grep for Math & Precision

# Division/multiplication order (rounding direction matters)
grep -RIn "\\\\/\\\\|\\\\*\\\\|%" contracts/

# Overflow possibilities
grep -RIn "unchecked" contracts/

# Precision handling
grep -RIn "1e18\\\\|10\\\\*\\\\*\\\\|decimals" contracts/

Grep for State & Reentrancy

# State variables
grep -RIn "mapping\\\\|storage" contracts/

# Protection present — or notably absent
grep -RIn "nonReentrant\\\\|ReentrancyGuard" contracts/

# Balance mutations
grep -RIn "balanceOf\\\\[.*\\\\]\\\\s*[+-=]" contracts/

Grep for Signatures, Proxies, and Flash Loans

# Signature verification
grep -RIn "ecrecover\\\\|signature\\\\|ECDSA" contracts/

# Replay protection
grep -RIn "nonce\\\\|deadline\\\\|expiry" contracts/

# Domain separation
grep -RIn "DOMAIN_SEPARATOR\\\\|EIP712" contracts/

# Proxy patterns
grep -RIn "implementation\\\\|upgradeTo\\\\|Proxy\\\\|ERC1967\\\\|UUPS" contracts/
grep -RIn "initialize\\\\|initializer\\\\|reinitializer" contracts/

# Flash loan callbacks
grep -RIn "flashLoan\\\\|flash\\\\|callback\\\\|executeOperation" contracts/

Grep for Permissions & Role Management

grep -RIn "grant\\\\|revoke\\\\|role\\\\|ROLE" contracts/
grep -RIn "owner\\\\s*=\\\\|admin\\\\s*=" contracts/
grep -RIn "renounce\\\\|transferOwnership" contracts/
grep -RIn "AccessControl\\\\|Ownable" contracts/
grep -RIn "hasRole\\\\|_checkRole\\\\|onlyRole" contracts/
grep -RIn "DEFAULT_ADMIN_ROLE" contracts/

For Non-Solidity Projects

If the codebase is Rust/Solana:

find . -type f -name "*.rs"
grep -RIn "^[[:space:]]*pub fn " .
grep -RIn "#\\\\[account\\\\(" .                     # Account constraints
grep -RIn "has_one\\\\|constraint\\\\|seeds" .       # Validation rules
grep -RIn "invoke\\\\|invoke_signed" .            # CPI calls
grep -RIn "unchecked\\\\|unsafe" .                # Unsafe blocks
grep -RIn "find_program_address" .             # PDA derivation
grep -RIn "unwrap\\\\|expect" .                   # Panic points
grep -RIn "as u64\\\\|as u128\\\\|try_into" .        # Type casting

If the codebase is Move (Aptos/Sui):

find . -type f -name "*.move"
grep -RIn "public entry\\\\|public fun" .
grep -RIn "move_to\\\\|move_from\\\\|borrow_global" .
grep -RIn "signer::address_of" .
grep -RIn "coin::transfer\\\\|coin::withdraw" .
grep -RIn "abort\\\\|assert!" .

If the codebase is Cosmos/Go:

find . -type f -name "*.go"
grep -RIn "func.*Msg.*Handler\\\\|MsgServer" .
grep -RIn "ValidateBasic\\\\|GetSigners" .
grep -RIn "panic\\\\(" .
grep -RIn "SetValue\\\\|GetValue\\\\|Delete" .
grep -RIn "Iterator\\\\|Iterate" .               # Unbounded iteration risk

Phase 2: Read Interfaces First — Always

This is the single most important habit you can build as an auditor.

Interfaces are contracts stripped down to their API. They show you exactly what the system claims to do, without the noise of how it does it:

interface ILendingPool {
    function deposit(address asset, uint256 amount, address onBehalfOf) external;
    function borrow(address asset, uint256 amount, uint256 interestRateMode, address onBehalfOf) external;
    function withdraw(address asset, uint256 amount, address to) external returns (uint256);

    event Deposit(address indexed reserve, address user, uint256 amount);
    event Borrow(address indexed reserve, address user, uint256 amount, uint256 borrowRateMode);
    event Withdraw(address indexed reserve, address user, address indexed to, uint256 amount);
}

From interfaces alone, you can answer: what are the entry points for value flow, what state transitions are possible, and what does the system promise to do? The gap between what interfaces promise and what implementations deliver is where many vulnerabilities live.

Phase 3: Understand the Data Model

Before reading a single function, map the state. Where does value live? Who controls what?

// Where is the money?
mapping(address => mapping(address => uint256)) public userCollateral;
mapping(address => uint256) public totalDeposits;

// Who has authority?
address public owner;
mapping(address => bool) public operators;

// What is the system's current state?
enum Status { Active, Paused, Deprecated }
Status public currentStatus;

When you understand the data model, you can reason about invariants — properties that should always be true. In a lending protocol, total borrows should never exceed total deposits scaled by the collateral factor. In a DEX, the product of reserves should only increase. In a staking contract, the sum of all user balances should equal the contract’s token balance.

Write these invariants down before reading the functions. Then check whether the functions actually maintain them.

Phase 4: Analyze Functions Systematically

When you finally read functions, read them as state machines, not as code. Every function is a transition: it takes some state, applies conditions, and produces new state.

The pattern to look for in every function:

CHECKS       → Are the preconditions validated?
EFFECTS      → What state changes?
INTERACTIONS → What external calls occur?

The classic reentrancy vulnerability exists precisely because interactions happened before effects — external calls were made while state was still inconsistent. When you see INTERACTION before EFFECT, that’s a flag.

For each function, ask: who can call this (does access control match documentation), what can go wrong with the inputs (zero values, max values, malformed addresses, re-entrant calls), what state does this modify (is there anything it shouldupdate that it doesn’t), does every external call behave as assumed (what happens if it reverts, returns false, or is malicious), and what is the worst case for an adversarial caller.

Phase 5: Follow the Money

This deserves its own phase because it’s the most direct path to critical vulnerabilities.

Pick every code path that moves tokens and trace it start to finish. Where does the transfer originate? Are balances updated before or after the transfer? Is the return value of transfer() / transferFrom() checked? Can the token be non-standard (fee-on-transfer, rebasing, ERC-777 with hooks)? What happens if the transfer fails silently?

Non-standard tokens are responsible for a disproportionate number of real-world exploits. The assumption that transfer()always behaves like a vanilla ERC-20 is wrong in enough cases to check every time.

# Find all transfer-related code
grep -rn "transfer\\\\|send\\\\|call{value\\\\|withdraw\\\\|deposit\\\\|mint\\\\|burn" --include="*.sol" .

# Look for fee-on-transfer and rebasing token assumptions
grep -RIn "fee\\\\|tax\\\\|FEE\\\\|rebase\\\\|elastic\\\\|share" contracts/

# Check return value handling
grep -RIn "bool.*=.*transfer\\\\|require.*transfer\\\\|SafeERC20" contracts/

Phase 6: Static Analysis

Run automated tools before deep manual review. They clear the low-hanging fruit and surface interesting areas worth focusing on.

# Slither — 80+ vulnerability detectors
slither .
slither . --print human-summary
slither . --print contract-summary
slither . --print function-summary
slither . --print call-graph

# Save output for reference
slither . --print human-summary > slither_report.txt

# Mythril — symbolic execution, better for targeted function analysis
myth analyze contracts/Contract.sol

# Solhint — linting with security rules
solhint 'contracts/**/*.sol'

# Semgrep — pattern-based, works across languages
semgrep --config=auto .
semgrep --config=p/smart-contracts .

# Aderyn — Rust-based Solidity analyzer
aderyn .

# 4naly3er — automated finding generator
4naly3er .

For Rust/Solana:

cargo clippy -- -W clippy::all
cargo audit
soteria -analyzeAll .
anchor verify <program-id>

For Move:

move prove
aptos move prove --named-addresses addr=default

Phase 7: Architecture Visualization

Generate visual maps before committing to deep manual review. A call graph reveals dependencies you’d spend hours tracing manually.

# Surya — call graphs, inheritance, function summaries
surya graph contracts/**/*.sol | dot -Tpng > callgraph.png
surya inheritance contracts/**/*.sol | dot -Tpng > inheritance.png
surya describe contracts/**/*.sol
surya mdreport report.md contracts/**/*.sol

# Sol2uml — UML class and storage diagrams
sol2uml class contracts/
sol2uml storage contracts/Contract.sol

# Storage layout — critical for upgrade audits
forge inspect Contract storage-layout --pretty

# Function selector mapping — for collision attacks
forge inspect Contract methodIdentifiers
cast sig "functionName(uint256)"
cast 4byte 0x12345678

# Flatten for external tools
forge flatten src/Contract.sol > flat.sol

Phase 8: Dynamic Analysis

Fuzzing

Fuzzing finds edge cases that manual review misses. Run it after you understand the system well enough to write meaningful invariants.

# Foundry built-in fuzzing
forge test --fuzz-runs 10000
forge test --fuzz-runs 100000 --fuzz-seed 12345

# Echidna — property-based fuzzing
echidna . --config echidna.yaml
echidna . --contract ContractTest --test-mode assertion

# Medusa
medusa fuzz

Invariant Testing

forge test --match-test invariant
forge test --match-contract InvariantTest

Fork Testing

# Test against real mainnet state — catches integration issues
forge test --fork-url $ETH_RPC_URL
forge test --fork-url $ETH_RPC_URL --fork-block-number 18000000

Existing Test Suite

# Run with full verbosity — failing tests are signals
forge test -vvv
cargo test

# Check coverage — low coverage means risky areas
forge coverage
forge coverage --report lcov
cargo tarpaulin

Phase 9: Project-Specific Patterns

Different protocol types have distinct vulnerability patterns. Once you know what you’re auditing, grep for its specific footprint.

DEX

grep -RIn "swap\\\\|addLiquidity\\\\|removeLiquidity" .
grep -RIn "getAmountOut\\\\|getAmountIn\\\\|reserve" .
grep -RIn "slippage\\\\|minAmount\\\\|deadline" .      # MEV protection
grep -RIn "K\\\\s*=\\\\|invariant" .                   # Constant product formula
grep -RIn "sync\\\\|skim" .                          # Uniswap-style patterns

Lending

grep -RIn "borrow\\\\|repay\\\\|liquidat" .
grep -RIn "collateral\\\\|healthFactor\\\\|ltv\\\\|LTV" .
grep -RIn "interestRate\\\\|utilization" .
grep -RIn "oracle\\\\|getPrice" .
grep -RIn "bad debt\\\\|shortfall" .

Staking & Vesting

grep -RIn "stake\\\\|unstake\\\\|reward\\\\|emission" .
grep -RIn "rewardPerToken\\\\|rewardPerShare\\\\|earned" .
grep -RIn "vest\\\\|cliff\\\\|linear\\\\|schedule\\\\|revoke" .
grep -RIn "lockup\\\\|unbonding" .

Governance

grep -RIn "propose\\\\|vote\\\\|execute\\\\|cancel" .
grep -RIn "quorum\\\\|threshold" .
grep -RIn "timelock\\\\|delay" .
grep -RIn "delegate\\\\|snapshot\\\\|checkpoint" .

Bridge

grep -RIn "lock\\\\|unlock\\\\|relay\\\\|message" .
grep -RIn "proof\\\\|merkle\\\\|verify" .
grep -RIn "chainId\\\\|srcChain\\\\|dstChain" .
grep -RIn "nonce\\\\|messageId\\\\|finality" .

Vault / ERC-4626

grep -RIn "deposit\\\\|withdraw\\\\|shares\\\\|assets" .
grep -RIn "convertToShares\\\\|convertToAssets" .
grep -RIn "strategy\\\\|harvest\\\\|compound" .
grep -RIn "totalAssets\\\\|totalSupply\\\\|ERC4626" .

Phase 10: Build the Attack Surface

At this point you have a working mental model. Now shift perspective entirely.

Stop thinking as a user. Think as an attacker.

What invariant breaks? If you had $10M and unlimited flash loans, what would you try? Price manipulation? Reentrancy? Sandwich attacks? Which invariant could you violate?

What’s the worst input? Try to find inputs the contract doesn’t anticipate: amount = 0, amount = type(uint256).max, address = address(this), address = address(0).

What external dependencies are trusted? Price oracles, other protocols, external tokens — each is an attack surface. What happens if they behave adversarially?

What are the admin privileges? Can the owner rug users? Can an admin drain funds? Centralization risks are real risks.

What are the upgrade risks? Storage collisions between versions, function selector clashes, missing reinitializer checks, state migration correctness.

Write a PoC for every finding you believe is real:

# Verify impact with a concrete exploit test
forge test --match-test testExploit -vvvv

Dependency Analysis

# Find external dependencies
grep -RIn "@openzeppelin\\\\|@solmate\\\\|@chainlink" .
grep -RIn "import.*from" .
cat package.json | jq '.dependencies'

# Check for known vulnerabilities
npm audit
cargo audit

# Verify OZ version — many vulnerabilities are version-specific
cat node_modules/@openzeppelin/contracts/package.json | grep version
npm ls @openzeppelin/contracts

Documentation Review

# Find specs and design docs
find . -name "README*" -o -name "SPEC*" -o -name "DESIGN*"
find . -path "*/docs/*" -type f

# NatSpec comments — compare with what code actually does
grep -RIn "@notice\\\\|@dev\\\\|@param\\\\|@return" contracts/

# Find developer concerns left explicitly in code
grep -RIn "TODO\\\\|FIXME\\\\|HACK\\\\|XXX\\\\|BUG" .
grep -RIn "temporary\\\\|workaround\\\\|hotfix" .

# Specification language in docs — these become invariants to test
grep -RIn "should\\\\|must\\\\|always\\\\|never\\\\|cannot\\\\|shall" docs/
grep -RIn "invariant\\\\|guarantee\\\\|require\\\\|ensure" docs/

The gap between what documentation says and what code does is one of the most reliable places to find vulnerabilities. If a comment says “this function can only be called once” and there’s no enforcement mechanism — that’s a finding.

The Anti-Patterns That Lead Auditors Astray

Reading line-by-line. This optimizes for coverage, not understanding. You can read every line and miss a critical vulnerability because you never understood the system’s invariants.

Skipping tests. Tests are the developer’s threat model, written in executable form. A test that uses vm.prank(attacker) is telling you exactly what the developer was worried about. Read the tests before you read the implementation.

Auditing in isolation. Every external protocol interaction is a dependency. If a contract integrates Chainlink, Uniswap, or Aave, you need to understand not just the integration but the edge cases of those external systems.

Trusting comments. Comments describe developer intent, not system behavior. The most interesting bugs often live in the gap between what a comment says a function does and what the code actually does.

Skipping grep. A five-minute grep session over the full codebase gives you a map. Starting without that map means you’re navigating blind.

Manual Review Checklist

Things grep cannot find for you — these require reading and reasoning:

A Practical Audit Template

Fill this out before writing a single finding:

Pre-Audit Checklist

Before starting manual review, verify you can check every box:

Speed-running this checklist is how critical issues get missed. The vulnerabilities don’t hide in the complex code — they hide in the assumptions you didn’t question.

Tools Installation Reference

# Foundry (Solidity testing, fuzzing, fork testing)
curl -L <https://foundry.paradigm.xyz> | bash && foundryup

# Slither (static analysis)
pip install slither-analyzer

# Mythril (symbolic execution)
pip install mythril

# Echidna (fuzzing) — download from github.com/crytic/echidna/releases

# Surya (visualization)
npm install -g surya

# Sol2uml (UML diagrams)
npm install -g sol2uml

# Solhint (linting)
npm install -g solhint

# Semgrep (cross-language pattern analysis)
pip install semgrep

# Cargo audit (Rust dependency vulnerabilities)
cargo install cargo-audit

Final Thought

The best auditors aren’t the ones who can read the most code the fastest. They’re the ones who can build the most accurate mental model of a system and reason clearly about its failure modes.

Slow down at the beginning. Understand the architecture before the implementation, the invariants before the functions, the threat model before the code. Run your grep pass. Generate your call graph. Read the tests. Then read the code.

The vulnerabilities will become obvious once you understand the system. They almost always do.