Crypto Training

A Practical Smart Contract Audit Workflow: From Threat Model to Finding

Good audits are structured: define invariants, map entry points, test the scary parts, and write findings that lead to fixes.

Crypto Training2026-02-0910 min read

Audit workflow illustration

Auditing is not a personality trait. It's a workflow.

If you treat audits as “read code and vibe”, you get inconsistent results: lots of noise, missed high-impact issues, and findings that don’t translate into fixes.

If you treat audits as a sequence of constraints and experiments, you produce repeatable findings and you can explain why they matter.

This post is a pragmatic process for real codebases: multi-contract systems, external integrations, upgrades, and MEV.

Step 0: define what you are protecting#

Write down the assets and guarantees.

I literally write them as sentences and keep them at the top of my notes.

Examples:

  • users can always withdraw (liveness)
  • accounting cannot be inflated (safety)
  • only governance can change parameters (auth)
  • price reads cannot be manipulated cheaply (economic security)

These become your invariants.

If the spec doesn’t state invariants, you must infer them from behavior, tests, and the project’s economics. A surprising number of incidents are “the code did what it said, but the team thought it said something else.”

Step 1: map entry points and privilege#

List every externally callable function that changes state.

Then categorize by who can call it.

CategoryWhy it mattersCommon failure
permissionlessbiggest attack surfacemissing checks, unbounded work
role-gatedprivilege mistakes are catastrophicrole misconfig, bypass
admin-onlygovernance is code + opsunsafe upgrades, key risk
contract-only“onlyContract” is brittlecallable via wrappers / delegatecall

Most critical bugs are “permissionless state change meets external call”.

If you do nothing else, do this: enumerate the state-changing entry points and mark “external calls inside accounting.” That alone catches a lot.

Step 2: draw the external-call graph#

Do not start with line-by-line reading.

Start with the edges that cross trust boundaries:

  • token calls
  • oracle calls
  • callbacks/hooks
  • low-level calls
  • delegatecalls

This graph tells you where invariants can be broken.

A good audit note looks like:

  • deposit() calls token transferFrom and fee module
  • withdraw() calls token transfer and optional hook
  • liquidate() calls oracle, then transfers, then emits

Once you have that map, you know where to spend attention.

Step 3: derive invariants (make them testable)#

A useful invariant is something you can falsify.

Bad invariant: “the protocol is safe.”

Good invariants:

  • total shares are consistent with total assets (under defined rules)
  • debt cannot decrease except via repayment
  • message ids are consumed exactly once
  • privileged actions require an explicit role

If you can’t state invariants, you can’t know what “broken” looks like.

A trick that works#

If you’re stuck, phrase invariants as “this variable can only move in these directions under these conditions.”

  • “fee growth is monotonic”
  • “collateralization can only decrease when price decreases”
  • “a message id can only be consumed once”

This converts ambiguity into checks.

Step 4: isolate the highest-yield bug classes#

This is where auditors make money.

Focus review on:

  • external calls in the middle of accounting
  • upgradeability and initialization
  • signature verification and replay boundaries
  • oracle reads and price assumptions
  • any loop over state that can grow
  • “best effort” calls that ignore return values

These are the places production systems bleed.

Step 5: turn hypotheses into tests#

If something feels scary, write a test.

  • exploit POC
  • fuzz test
  • invariant check

This does two things:

  • forces clarity (you either found a path or you didn’t)
  • produces a regression artifact the project can keep

What tests I want to see#

RiskTest styleWhy
rounding/accountingproperty + fuzzrounding bugs hide in corners
reentrancyattacker harness + tracemakes control flow explicit
token weirdnessdelta-based assertionskills fee-on-transfer assumptions
upgradesupgrade simulationcatches layout drift
oracle manipulationadversarial scenariosmakes economics explicit

A strong audit builds a small suite of “fear tests” that encode the threat model.

Step 6: severity is about impact and exploitability#

A lot of reports confuse “scary” with “critical”.

I use a simple rubric:

SeverityTypical impactTypical exploitability
Criticalfund loss / takeoverfeasible on mainnet
Highmajor fund loss / permanent DoSplausible with mild assumptions
Mediumlimited loss / temporary DoSneeds timing/conditions
Lowfootgun / defense-in-depthunlikely or minor

The important part is to write down the assumptions.

“Critical if attacker can choose the token” is different from “critical if only governance can set token and governance is a timelock.”

Step 7: write findings that lead to fixes#

A good finding is not “this might be dangerous”.

A good finding is an engineer handing another engineer a reproducible bug.

A template that works:

  • Impact: what goes wrong (fund loss, DoS, privilege escalation)
  • Root cause: why it happens
  • Preconditions: when it is reachable
  • Exploit sketch: minimal steps
  • Recommendation: specific fix + tradeoffs

Example finding (short)#

Impact: attacker can inflate shares and steal assets from other depositors.

Root cause: shares are minted from the user-provided amount instead of actual received amount; fee-on-transfer tokens break the accounting.

Preconditions: attacker can deposit a fee-on-transfer token, or token can be swapped to such a token via governance.

Exploit sketch: deposit amount, receive amount - fee, but mint shares as if amount arrived, then withdraw more than contributed.

Recommendation: mint shares from balance delta (after - before), or restrict tokens via adapters/allowlist.

This is actionable because it is a concrete mismatch between an invariant and an assumption.

Step 8: verify the fix and lock regressions#

The fix is not “merged”. The fix is:

  • verified by tests
  • verified by reasoning (invariants now hold)
  • protected by regression tests

Audits should reduce future risk, not just produce a report.

Step 9: don’t ignore operational risk#

Many “Solidity incidents” are actually operational failures:

  • upgrade keys compromised
  • timelocks bypassed via misconfig
  • guardians abused
  • deploy scripts wrong

As an auditor, you don’t own ops. But you should surface when protocol safety depends on flawless ops.

A good audit explicitly states:

  • where admin power exists
  • what happens if admins are malicious
  • what happens if admins are compromised

Step 10: keep an audit log, not just a report#

A report is a snapshot.

An audit log is the reasoning trail.

I like logs that include:

  • invariants
  • entry point map
  • external-call map
  • hypotheses and tests
  • decisions (“we accept this risk because …”)

That’s what lets teams learn, not just patch.

Step 11: use tooling to buy time, not to outsource thinking#

Static analysis and traces are force multipliers, not auditors.

I use tools for three things:

  1. Coverage: find the functions and edges I might miss by eye.
  2. Triage: confirm whether a hypothesis is reachable.
  3. Evidence: capture traces and minimal repros.

Here’s a sane tool map:

ToolWhat it’s good forWhat it’s bad for
Slitherbroad patterns, footguns, quick winsnuanced invariants, economics
Foundryreproducing and fuzzingfinding issues you never hypothesize
Tenderly / tracesunderstanding execution pathsproving economic safety
Semgrepproject-specific pattern huntsinter-procedural reasoning without good rules

The trap is to run tools and turn alerts into findings.

The better workflow is: use tools to validate or falsify your threat model.

Step 12: write “what breaks” before “what’s wrong”#

Engineers respond to impact.

If your finding begins with “the code does X,” the team will argue about whether X matters.

If your finding begins with “a user can steal Y under conditions Z,” the discussion becomes concrete.

An effective ordering inside a finding:

  1. Impact: what breaks (liveness, fund safety, auth)
  2. Exploit path: minimal steps
  3. Root cause: the specific assumption that failed
  4. Fix: the narrowest change that restores the invariant
  5. Regression: the test that prevents the bug from returning

This is how you turn a report into a security improvement, not just a PDF.

Step 13: the fastest way to level up is to do postmortems#

When you read an incident report, don’t just collect the headline.

Write down:

  • what invariant failed
  • what trust boundary was crossed
  • what assumption the code made
  • what tests would have caught it

After 20 postmortems, you start seeing the same primitives everywhere: reentrancy edges, replay boundaries, oracle cost mistakes, and upgrade auth.

Step 14: context building (how to not drown in a big repo)#

Large codebases kill audits by overwhelming you.

The trick is to build context deliberately:

  • start from the deployment artifacts and configuration (what is actually deployed?)
  • identify the “asset boundary” (where funds or authority live)
  • follow the code paths that touch those assets

A practical approach:

QuestionWhat you look at first
Where can funds leave?withdraw/transfer paths, settlement, rescue functions
Who can change rules?governance/admin roles, upgrade hooks
What can revert and freeze progress?token calls, hooks, oracle reads
What is assumed honest?keepers, relayers, off-chain signers

If you can answer those four early, your line-by-line reading becomes targeted instead of random.

Step 15: a simple day-by-day audit cadence#

For a typical multi-contract protocol:

  • Day 1: entry points, privilege map, external-call graph
  • Day 2: accounting invariants, oracle and token assumptions
  • Day 3: exploit attempts + fuzz harnesses for the scary paths
  • Day 4: upgradeability, roles, configuration review
  • Day 5: write findings, reproduce, fix-review, regression tests

This cadence is not a rule. It’s a way to ensure you don't spend 80% of time “reading” and 20% validating the parts that can actually fail.

Step 16: treat integrations as adversarial APIs#

Most DeFi systems are glue:

  • tokens
  • oracles
  • bridges
  • DEX routers
  • staking modules

Each integration is an API with failure modes: reverts, weird return values, hooks, stale data, and gas griefing.

When auditing integrations, I like to write a short “contract” for each dependency:

  • what does it guarantee?
  • what can it do unexpectedly?
  • what is your safe failure mode?

This forces you to design around reality instead of the happy path.

If a dependency can revert, your protocol inherits that revert as a liveness risk.

If a dependency can reenter (hooks, callbacks, token transfers), your protocol inherits that as a control-flow risk.

Most “mysterious” incidents are just these inherited risks showing up at the worst possible moment, where the mempool is hostile and transactions race.

Watch: audit techniques and tools (Secureum)#

If you prefer video, this Secureum lecture is a solid companion to the workflow above: how to think about attack surfaces, how to use tools as filters, and how to avoid “random reading”.

Further reading#