blockchainsv
Security & Auditing·July 29, 2026·16 min read

Blockchain audit steps: how to prepare your code for review

A blockchain audit does not begin when an auditor opens the repository. It begins when the protocol team stops changing the system long enough for its security properties to be examined.

Blockchain audit steps: how to prepare your code for review

This distinction is operational, not semantic. A review against an unfrozen branch produces findings against code that may no longer exist at deployment. A patch merged halfway through the engagement can invalidate an earlier data-flow conclusion. A late scope expansion changes the attack surface, the schedule, and the cost. The audit becomes a moving-target exercise. It is not an audit.

The numbers explain why preparation matters. A simple token review may take two to five days and cost $5,000–$15,000. A standard DeFi protocol can require three to six weeks and $50,000–$100,000. Bridges, ZK-rollups, and other high-complexity systems can require months and budgets above $150,000. Review time is consumed by code paths, trust boundaries, state transitions, and cross-contract assumptions. Not by Solidity syntax alone.

A prepared repository lets the auditor test invariants. An unprepared repository forces the auditor to reconstruct product intent from fragments.

An audit cannot prove security. It can only test a defined system against defined assumptions at a defined commit.

Set the scope before anyone reads the code

The first blockchain audit step is to define exactly what is being reviewed. “The protocol” is not a scope. Neither is a repository URL.

A usable scope binds the engagement to a commit hash. Every in-scope contract, library, deployment script, proxy implementation, and inherited dependency must be identified. If the system uses deployed external contracts—price feeds, bridges, DEX routers, permit implementations, staking wrappers—their addresses and interface assumptions belong in the scope documentation even when their source is not being reviewed.

The commit hash is the boundary. It creates a deterministic object for analysis.

A code freeze does not mean development stops forever. It means security-critical changes stop on the audit branch. Teams can continue product work elsewhere, but the audited branch must remain stable until findings are resolved and the final review is complete. If a change is necessary, classify it correctly:

1. Non-functional change. Comment corrections, formatting changes, or test labels may not alter execution. They should still be disclosed. Diff review is cheap. Hidden changes are not.

2. Localized security fix. A finding remediation can alter one function while preserving the architecture. It requires a patch review and regression tests.

3. Semantic change. New permissions, altered fee logic, changed collateral rules, added oracle dependencies, or new external calls change the threat model. This is a rescope event.

4. Deployment change. A new proxy admin, a different initializer payload, altered constructor arguments, or a replacement oracle address can invalidate correct source-code conclusions. Deployment configuration is code in operational form.

Do not send an auditor the default branch and promise that “the final changes will be small.” Small changes frequently sit on privileged paths. A one-line modifier change can convert a permission boundary into an unrestricted state mutation.

The scope should also state what is excluded. If the frontend constructs signed messages, but signature verification is on-chain, the frontend may be excluded while the EIP-712 domain separator, nonce handling, deadline logic, and signer recovery remain in scope. If a keeper executes liquidation functions, the keeper infrastructure may be excluded while the contract’s assumptions about call timing and caller incentives remain in scope.

This is where many smart contract auditing processes fail. The team supplies contracts. The auditor needs a system.

Write the specification auditors will try to break

Source code describes implementation. It does not reliably describe intent.

An auditor can infer that withdraw() transfers assets after decrementing a balance. That does not establish whether users should be able to withdraw during a paused state, whether withdrawal fees are rounded up or down, whether a queued withdrawal remains valid after a vault loss, or whether the system is intended to remain solvent under a stale oracle price.

Those are protocol rules. They must be written down.

Documentation for a blockchain audit should include functional requirements, architecture, trust assumptions, role definitions, deployment parameters, and NatSpec for public and external functions. The standard is not literary quality. The standard is whether an independent reviewer can derive the intended invariants without guessing.

For every asset-bearing module, document at least the following:

  • Asset accounting model. Define what the contract considers deposited, reserved, claimable, borrowed, accrued, and withdrawable. State the unit for each value: raw token units, shares, normalized wad values, or another fixed-point representation.
  • Solvency invariant. State the relationship that must hold between liabilities and controlled assets. If insolvency is possible by design, define the loss-allocation rule instead of leaving it implicit.
  • Authorization model. Identify every actor capable of changing parameters, upgrading code, pausing functions, moving funds, setting oracle sources, or granting roles.
  • External trust boundaries. List every token, oracle, callback receiver, bridge endpoint, router, relayer, and off-chain signer the contracts depend on. A contract call is an attack vector unless its behavior is constrained.
  • Timing model. Document epochs, cooldowns, lock periods, voting delays, price freshness limits, and settlement windows. Block timestamps are not precise clocks. They are bounded inputs under validator control.
  • Failure behavior. Specify what happens if an oracle reverts, a transfer returns false, a token takes a fee, a callback reenters, a keeper does not act, or a price is stale.
  • Upgrade model. State whether the system is immutable, UUPS-based, transparent-proxy based, beacon-based, or controlled through another pattern. Include initializer order and storage-layout constraints.

NatSpec is not decoration. A public function without a precise comment forces the reviewer to reverse-engineer its preconditions and postconditions from branches. That consumes audit time and creates ambiguity around intended behavior.

Consider a function that liquidates an unhealthy account. Its documentation must establish more than “liquidates a position.” It should define the health calculation, oracle freshness requirement, close factor, rounding direction, incentive recipient, residual debt behavior, and conditions under which liquidation must revert. Without this, the reviewer may identify a behavior that appears unsafe while the team claims it is intended. That argument is not a resolution. It is evidence that the specification arrived late.

Convert requirements into explicit invariants

The most useful audit documentation is a list of invariants. These are statements that must remain true after every permitted state mutation.

Examples:

  • Total vault shares equal the sum of user shares, subject to an explicitly defined rounding remainder.
  • No account can redeem more underlying assets than its share balance permits.
  • A borrower cannot increase debt if the resulting health factor is below the protocol threshold.
  • A paused contract cannot create new liabilities, even through a secondary entry point.
  • An upgrade cannot replace an implementation unless the authorized governance path has completed.
  • A signature cannot be replayed across chains, contracts, nonces, or expired deadlines.
  • A withdrawal queue cannot release the same claim twice.

Auditors use these statements to form test cases, trace state mutations, and identify asymmetric paths. Developers should use them first.

If an invariant cannot be stated in one sentence, it is unlikely to survive every execution path.

Make tests carry evidentiary weight

High test coverage is useful only when it exercises meaningful behavior. A coverage report above 90% can indicate readiness. It can also indicate that a test suite executed trivial lines while avoiding the protocol’s critical transitions.

The target is not a percentage in isolation. The target is a test suite that demonstrates expected behavior, rejects invalid behavior, and preserves previously fixed behavior.

A credible pre-audit suite should cover three categories.

Expected execution

These are the ordinary flows: deposit, borrow, repay, stake, claim, vote, withdraw, settle, upgrade, and emergency pause. The tests should assert balances, events, access control, and final state. Merely confirming that a transaction does not revert is weak evidence.

Boundary execution

Most accounting defects live at boundaries:

  • zero-value deposits and withdrawals;
  • minimum and maximum amounts;
  • one-unit rounding cases;
  • values around collateral and liquidation thresholds;
  • timestamp boundaries around deadlines and epochs;
  • empty pools, first deposits, and last withdrawals;
  • fee-on-transfer and non-standard ERC-20 behavior;
  • stale or zero oracle answers;
  • repeated calls after state has already been consumed.

The first depositor in a share-based vault deserves dedicated tests. So does the last redeemer. These positions often expose division-by-zero failures, inflation attacks, and rounding drift.

Adversarial execution

This is where audit preparation becomes security work rather than QA.

Build malicious fixtures. Use a callback-capable token. Create a receiver contract that reenters. Simulate an oracle that returns an old answer, a negative answer where the interface permits it, or an unexpected decimal scale. Test an ERC-20 that returns no boolean value, returns false, charges a transfer fee, or invokes a hook.

For a lending protocol, do not only test a healthy borrower repaying normally. Test collateral value falling between calls. Test a liquidator attempting to extract more collateral than the close factor permits. Test a user opening, partially repaying, and reopening a position around rounding boundaries. Test whether interest accrual is applied before or after a withdrawal that changes utilization.

Fuzz testing should target invariants, not random function calls alone. Stateful fuzzing is particularly valuable for protocols whose security depends on sequences: deposit, borrow, oracle update, partial repay, liquidation, withdrawal. A single function may be correct in isolation while the sequence violates an accounting constraint.

Test layerWhat it should establishCommon failure hidden by weak tests
Unit testsLocal function preconditions and postconditionsMissing access modifier, incorrect arithmetic branch
Integration testsCross-contract state transitionsWrong token approval flow, broken callback handling
Fuzz testsInvariants across broad input rangesRounding loss, underflow-adjacent logic, unreachable assumptions
Stateful testsInvariants across transaction sequencesDouble claims, stale cached values, sequence-dependent insolvency
Fork testsBehavior against real external interfacesOracle interface mismatch, router assumptions, token incompatibility
Regression testsFixed findings remain fixedReintroduced vulnerability after refactoring

A test suite should also be reproducible. Pin compiler versions. Pin dependency versions. Document the command that runs the full suite. Remove tests that require an undocumented API key, a manually funded address, or a developer’s local configuration. If the auditor cannot execute the tests deterministically, the suite becomes a demonstration rather than evidence.

Run static analysis before paying for manual review

Static analysis is not an audit. It is pre-audit hygiene.

Slither is effective for rapid checks in local development and CI. It can surface patterns such as dangerous inheritance, uninitialized state, shadowed variables, suspicious low-level calls, reentrancy candidates, incorrect equality checks, and missing event emissions. Its value is speed. A developer can run it repeatedly while changing code.

Mythril operates differently. It uses symbolic execution to explore execution paths and reason about constraints. This can expose deeper issues in small and bounded components, especially where a vulnerability depends on a particular sequence of conditions. It can also produce false positives. In secure contracts, reports may require manual triage because the tool cannot fully model protocol intent, external contracts, or environmental constraints.

That limitation is expected. Do not suppress a result because it is inconvenient. Classify it.

A disciplined pre-audit workflow looks like this:

1. Run Slither against the frozen scope and review every detector output. Mark each item as fixed, accepted with justification, out of scope, or false positive with evidence.

2. Run Mythril selectively against high-risk contracts and externally callable asset-moving functions. Broad symbolic execution over an entire protocol often creates noise and burns time.

3. Review compiler warnings and remove dead code. Dead code expands the reviewer’s workload and can become live code after an upgrade or an inheritance change.

4. Inspect the dependency graph. A secure local contract can still be unsafe if it trusts an outdated library, an unrestricted adapter, or an incorrectly configured proxy.

5. Commit the tool configuration and the triage notes. The auditor should see the output, the version used, and the reasoning behind each disposition.

Do not treat a clean scanner result as a security signal of sufficient strength to deploy. Automated tools cannot establish economic safety, validate off-chain trust assumptions, or decide whether a governance operation is acceptable. They identify patterns. Manual review determines exploitability.

The distinction matters because many high-impact exploits are valid executions of flawed protocol logic. No compiler warning identifies a collateral factor that is economically unsafe under a volatile oracle. No generic detector can decide whether a cross-chain message should be accepted after a source-chain reorganization. The code executes exactly as written. The specification is wrong.

Trace external calls and enforce Checks-Effects-Interactions

Reentrancy remains one of the clearest examples of a failed execution order.

The vulnerable shape is simple: a contract validates a caller, transfers control to an external address, and only afterward updates its own accounting. The external recipient reenters while the original balance or claim remains intact. The second call passes checks that should have failed after the first state mutation.

The Checks-Effects-Interactions pattern is the baseline defense:

1. Checks. Validate authorization, amounts, deadlines, pause state, and preconditions.

2. Effects. Update internal state. Debit balances. Mark claims consumed. Increment nonces. Record completed actions.

3. Interactions. Make external calls only after the state reflects the intended result.

The order is not stylistic. It controls what a reentrant call can observe.

A withdrawal function should reduce the user’s balance before transferring tokens. A claim function should mark a distribution as claimed before sending assets. A bridge withdrawal should consume the message identifier before invoking a receiver or releasing funds. If the external call fails, the EVM reverts the whole transaction and restores the prior state. There is no reason to preserve unsafe ordering for rollback purposes.

A reentrancy guard can provide a second control. It should not replace correct state ordering. Guards may be omitted from a secondary entry point, misapplied across proxy boundaries, or prevent legitimate composability without addressing the underlying inconsistent state.

The same analysis applies beyond obvious token transfers. Every external call is relevant:

  • call, delegatecall, and staticcall;
  • ERC-20 transfer and transferFrom;
  • ERC-721 and ERC-1155 safe transfers;
  • oracle reads through external aggregators;
  • DEX swaps and router callbacks;
  • flash-loan callbacks;
  • hook-based token standards;
  • contract creation that invokes constructors with external dependencies.

For each interaction, map the state visible before the call and after it. Then determine whether the callee can invoke any function that observes or mutates that state. This is the core reentrancy autopsy.

A contract may be non-reentrant at one function and still expose a cross-function vector. For example, withdraw() may use a lock while claimRewards() reads the same balance accounting without one. The attacker does not need to reenter the original function. They need a second path that observes an intermediate state.

Audit the people behind the roles

Privileged access is part of the attack surface. It is often the largest part.

An onlyOwner modifier does not describe a security model. It describes a single address with unspecified power. During a blockchain security audit, every privileged role must be enumerated with its capabilities and activation path.

This includes obvious roles such as owner, admin, guardian, governor, upgrader, pauser, minter, and treasury manager. It also includes less visible authority: an address allowed to set an oracle, alter a fee recipient, whitelist a router, register a bridge endpoint, change a signer set, or update an implementation.

For each role, document:

  • the functions it can invoke;
  • the state it can mutate;
  • whether it can move user assets directly or indirectly;
  • whether its actions are immediate or timelocked;
  • how the role is granted, revoked, and transferred;
  • whether a compromised role can be contained through pause controls;
  • whether an action emits an event sufficient for off-chain monitoring.

Two-step ownership transfer should replace one-step ownership assignment where practical. The pending owner accepts the role explicitly. This prevents a typo, an incorrect multisig address, or a non-recoverable contract address from silently becoming the controller.

Multisignature control reduces single-key risk. It does not eliminate governance risk. A multisig holding upgrade authority can still deploy malicious code if its threshold is compromised or its signers are colluding. Timelocks create observation time. They allow users, monitors, and security teams to inspect a queued operation before it executes. For upgradeable systems, this delay is not a convenience feature. It is a containment window.

The emergency pause requires equal scrutiny. A pause mechanism that only blocks deposits but permits borrowing does not contain a broken oracle. A pause function that can permanently trap withdrawals creates a separate governance risk. The intended failure mode must be explicit.

Centralization is not removed by hiding authority behind a modifier. It is measured by the worst state mutation that authority can execute.

Deliver an audit package, not a repository

Preparing for a blockchain audit means reducing ambiguity before the manual review begins. The package should be sufficient for an auditor to reproduce the build, understand the system, trace the trust boundaries, and test stated invariants.

The final pre-engagement gate is rigid:

  • Freeze the audited scope at a specific commit hash. Record every in-scope file and deployment artifact.
  • Provide architecture documentation showing contract relationships, asset flows, proxy paths, external dependencies, and off-chain components that influence on-chain decisions.
  • Write functional specifications and NatSpec for every public and external function. Define preconditions, postconditions, roles, and failure behavior.
  • List the protocol invariants. Include solvency, accounting, authorization, signature, timing, and upgrade invariants where applicable.
  • Reach high coverage, preferably above 90%, while proving edge cases and adversarial sequences rather than only normal execution.
  • Run Slither and triage its results. Use Mythril or equivalent symbolic analysis where bounded high-risk logic justifies it. Preserve outputs and explanations.
  • Test reentrancy with malicious callbacks and verify that state mutation precedes every external interaction.
  • Map every privileged role. Deploy critical authority behind multisig controls, timelocks, and two-step transfers where the threat model requires them.
  • Include deployment scripts, initializer arguments, chain configuration, oracle addresses, role assignments, and verification commands.
  • Keep a change log during the audit. Any semantic change after the freeze must trigger explicit reassessment.

An audit team should spend its time breaking assumptions, not locating them. That is the practical standard.

The audit report will still contain uncertainty. It should. Smart contracts operate inside adversarial environments, with external dependencies, governance processes, and future code changes that no point-in-time review can fully constrain. But a frozen scope, explicit invariants, executable tests, automated pre-screening, and a documented authority model turn that uncertainty into a tractable security review.

Anything less transfers discovery work to the audit window. That increases cost. More seriously, it leaves attack vectors undiscovered because the system was never defined precisely enough to test.

FAQ

Why is a code freeze necessary for a blockchain audit?
A code freeze ensures the auditor is reviewing a stable, deterministic object. Changes made during the engagement can invalidate previous findings and turn the audit into a moving-target exercise.
What should be included in the audit scope documentation?
The scope must be bound to a specific commit hash and include all contracts, libraries, deployment scripts, proxy implementations, and inherited dependencies. It should also explicitly state which components are excluded from the review.
What is the purpose of defining invariants in audit documentation?
Invariants are statements that must remain true after every state mutation, allowing auditors to form precise test cases and identify asymmetric paths that deviate from the protocol's intent.
How should a team handle privileged roles before an audit?
Teams should enumerate every privileged role, document its capabilities and activation paths, and implement security measures like multisig controls, timelocks, and two-step ownership transfers.
Why is the Checks-Effects-Interactions pattern important?
This pattern prevents reentrancy vulnerabilities by ensuring that internal state updates occur before any external calls are made, preventing attackers from observing or exploiting intermediate states.

By Caleb North