A smart contract audit is not a scan, and it is not a certificate. It is a structured, adversarial attempt to make your code do something it was never meant to do, carried out by people who have spent years watching how other people's code failed. The checklist below is the one our auditors work through on every engagement. It is long because the list of ways to lose money on-chain is long, and every item on it is here for the same reason: we have seen it cost somebody real funds.

We publish it for two audiences. If you are about to commission an audit, this tells you what a thorough one covers, so you can judge the proposals you receive. If you are an engineer, this is a self-review you can run before you spend a single euro on external help — and a well-prepared codebase gets a much better audit for the same budget.

1. Scope, build, and threat model

Before reading a line of code, we establish what we are actually auditing.

  • A frozen commit hash. Audits of moving targets are worthless. If the code changes during the review, the review restarts for the affected files.
  • A reproducible build and a green test suite. If the tests do not pass, we learn something about the engineering culture before we learn anything about the contracts.
  • A written threat model, even a short one. Who are the privileged actors? What is each one allowed to do? What is the worst thing that happens if each of them is compromised? What external systems does the protocol depend on, and what happens when they lie?
  • An explicit list of out-of-scope components, and a check that the dependencies you consider "trusted" actually deserve it. A protocol is only as safe as the weakest contract it calls.

The threat model is the item teams skip most often, and it is the one that most improves the audit. Half the critical findings we report are not bugs in the code — they are mismatches between what the code does and what the team believed it did.

2. Access control, privileges, and initialisation

Most catastrophic losses start with a missing modifier or a function that should never have been public. We enumerate every state-changing function and ask three questions: who can call it, under what conditions, and what is the blast radius if that caller's key leaks.

  • Map every onlyOwner, every role in AccessControl, and every timelock to a real human, multisig, or governance process. Roles that nobody can name are roles that nobody is watching.
  • Check initialisers on every upgradeable contract. An implementation contract that can still be initialised, or a proxy whose initialize() was never called, is an open door — this exact pattern has been exploited repeatedly against forks of well-known protocols.
  • Look for functions that are public but were meant to be internal: callbacks, hooks, _mint wrappers, emergency functions added late in development.
  • Verify that privilege changes are two-step where it matters. A single mistyped address in transferOwnership has bricked contracts holding tens of millions.
  • Assume the deployer key leaks. If that single assumption breaks the protocol, the protocol is one phishing email away from a total loss.

3. Arithmetic, accounting, and rounding

Solidity 0.8 removed silent overflow, and in doing so it removed the easy arithmetic bugs. What remains is harder to see and more expensive: rounding direction, unit confusion, and accounting that drifts from reality.

  • Trace every division. Ask which party benefits from the rounding. The protocol should round in its own favour on deposits and against the user on withdrawals, never the reverse.
  • Check share-to-asset conversions in every vault-style contract for the first-depositor inflation attack: an attacker deposits one wei, donates a large amount directly to the contract, and every subsequent depositor is rounded down to zero shares. Virtual shares or a minimum initial deposit are the standard mitigations.
  • Verify decimals handling for every token the contract will ever touch. A contract that assumes 18 decimals will mishandle USDC, and a contract that assumes decimals() exists will revert on tokens that do not implement it.
  • Compare internal accounting against actual balances. Any place where balanceOf(address(this)) is used as a source of truth is a place where a direct transfer can distort the protocol's view of the world.
  • Check for fee-on-transfer and rebasing tokens. If the protocol is not designed for them, it should refuse them explicitly rather than mis-account them silently.

4. External calls and reentrancy

Treat every external call as a transfer of control to an adversary, because that is exactly what it can be.

The classic mistake is a withdraw function that sends the funds with a low-level call and only then decrements the caller's balance. The recipient contract re-enters withdraw before the balance is updated, and the loop continues until the contract is empty. The fix is one line in the wrong order; the loss has run into hundreds of millions across the industry.

  • Apply checks-effects-interactions everywhere, not just in withdrawal functions. State must be updated before the call, not after.
  • Look for cross-function and cross-contract reentrancy. A nonReentrant modifier on one function does not protect a second function that reads the same state while the first is mid-call.
  • Look for read-only reentrancy. View functions that report stale prices or balances mid-transaction have been used to drain lending protocols that integrated an otherwise secure pool. If your protocol exposes a view that other protocols will trust, that view is part of your attack surface.
  • Check ERC-777, ERC-721 and ERC-1155 hooks. Token transfers that call back into the recipient are reentrancy vectors that many developers do not think of as external calls.
  • Verify return-value handling for low-level calls and for tokens that return false instead of reverting. Use a safe-transfer wrapper and check that it is actually used everywhere.

5. Upgrades, pausing, and emergency powers

Upgradeability is a privilege, and privileges are attack surface. Every upgrade mechanism gets the same scrutiny as every other privileged function, plus a few checks of its own.

  • Storage layout: confirm that no variable was reordered, removed, or changed in type between versions. A storage collision silently corrupts state and may not be noticed until funds are stuck.
  • Confirm that the implementation cannot selfdestruct and that no delegatecall target can be changed by an unprivileged path.
  • Check who can upgrade, how long the timelock is, and whether users can realistically exit during that window. A 24-hour timelock on a protocol where withdrawals take 7 days is theatre.
  • Review pause mechanics. Pausing deposits while leaving withdrawals open is usually correct; pausing withdrawals is a decision that should require more than one key.
  • Review emergency functions line by line. "Rescue" functions that can move arbitrary tokens are, by definition, a backdoor. If one must exist, it should be timelocked and loudly evented.

6. Oracles, prices, and economic assumptions

The code may be perfect and the protocol still insolvent. We spend a substantial part of every DeFi audit on the questions that do not appear in the Solidity at all.

  • Where does every price come from? Spot prices from an AMM pool are manipulable within a single transaction using a flash loan. Time-weighted prices resist that but lag real markets. Pushed oracle feeds can be stale. Each source needs its own sanity checks.
  • What happens when the oracle is stale, reverts, or returns zero? The answer should be "the protocol stops accepting that asset", not "the protocol treats it as free".
  • Can the attacker profit by being large? Liquidation mechanics, bonding curves, and incentive systems all have regimes where a sufficiently capitalised actor extracts value from everyone else. We model them.
  • Are there circular dependencies — a collateral asset whose price depends on the protocol that accepts it as collateral?
  • Does the protocol remain solvent under a sharp price move combined with network congestion? Liquidations that cannot be executed in time are bad debt.

7. Signatures, replay, and off-chain inputs

Any data that arrives from outside the chain — signatures, Merkle proofs, permit messages, relayed intents — must be bound tightly to where it is meant to be used.

  • Every signed message needs a nonce, a deadline, a chain ID, and the address of the verifying contract. Missing any one of these enables replay across time, chains, or contracts.
  • Check ecrecover for the zero-address return on invalid signatures and for signature malleability if the contract does not use a hardened library.
  • Verify Merkle proofs for leaf/node ambiguity and for proofs reusable across different trees.
  • Check that off-chain components that produce these inputs (keepers, relayers, sequencers) are in the threat model, because they are in the attack surface.

8. Gas, denial of service, and griefing

An attacker does not need to steal funds to hurt a protocol. Locking them, or making operations unaffordable, is often enough.

  • Unbounded loops over user-controlled arrays will eventually exceed the block gas limit and brick the function.
  • Withdrawals that depend on a push to an external address can be blocked by a recipient that reverts. Use pull patterns.
  • Check whether any critical operation can be front-run to make it fail: a liquidation that reverts because the attacker repaid one wei first, a governance proposal that cannot execute because its target was changed.

9. Code quality and the things that are not bugs yet

We report these at lower severity, but we report them, because they are where next year's critical finding is born.

  • Dead code, commented-out logic, and TODO markers in production contracts.
  • Events missing on privileged operations. If it is not evented, nobody is monitoring it.
  • Inconsistent use of libraries — one safe-transfer here, a raw call there.
  • Missing or misleading NatSpec. When the comment and the code disagree, one of them is a bug.
  • Test coverage that only exercises the happy path. We check the tests as carefully as the code, because they tell us what the team never thought about.

10. The operational layer

The contracts are only part of the system. On every engagement we also look at:

  • Deployment scripts. Hard-coded addresses, wrong constructor arguments, and forgotten initialize() calls happen at deploy time, not in the code review.
  • Key custody. Where do the admin keys live, who holds them, and what does the signing process look like? A perfect contract behind a hot wallet is not a secure protocol.
  • Monitoring. What alerts fire if a privileged function is called, if TVL moves by 20% in a block, or if the oracle deviates from the market?
  • Incident playbook. Who has the authority to pause, how do they reach each other at 3 a.m., and have they ever rehearsed it?

Where the money is actually lost

Ranked by how much money each class has actually cost, from what we see in post-mortems:

  • Access control and uninitialised proxies — critical; usually total loss.
  • Reentrancy, including read-only — critical; lending and vault protocols.
  • Oracle manipulation — critical; the engine behind most flash-loan attacks.
  • Share inflation and rounding — high; drained vaults, one depositor at a time.
  • Storage collisions on upgrade — high; bricked or corrupted state.
  • Signature replay — high; the same withdrawal, executed again and again.

If you only have time to check one thing before launch, check who can call what.

How we report, and what you should expect

Every finding we deliver has a severity, a clear description of the impact, the exact location in the code, and — for anything rated High or Critical — a working proof of concept. If an auditor tells you something is exploitable but cannot show you, be sceptical. If an auditor's report consists mostly of "informational" findings and tool output, you have paid for a scan.

After the fixes are in, we re-review every changed line. Fixes introduce bugs at roughly the same rate as original code, and a fix review is where we catch them.

Using this checklist yourself

Run through it before you book an audit. Write down your answers. The items you cannot answer are the ones to fix first — and they are the ones we will ask about on day one. A team that arrives with a threat model, a frozen commit, a role map, and honest answers to the oracle questions gets a deeper audit, faster, and usually a cheaper one. A team that arrives with "it's basically a fork of X" gets exactly the audit that phrase deserves.

If you want us to run it for you, our audit scope and process are described on the services page, and you can reach the team directly on Telegram.