Blockchain & Crypto
Smart Contract Audit Checklist: What to Review Before Mainnet
An audit is a gate, not a formality
Teams under launch pressure sometimes treat the security audit as a box to check before a token generation event. That mindset is exactly how preventable exploits make it to mainnet — the audit needs to be a real gate, with a mandate to block launch until findings are resolved, not just documented.
The distinction matters more than it sounds. A gate means the audit firm — or your internal security lead, if you're running the review in-house first — has the explicit authority to say 'not yet,' and the project timeline is built to absorb that answer without a scramble. Teams that build in a two-to-three week remediation-and-re-audit buffer after the initial findings land ship far fewer post-launch incidents than teams that schedule the audit as the last item before a fixed launch date, because the latter creates enormous pressure to downgrade severity ratings or ship with 'acceptable' known issues. If your launch date cannot move to accommodate what the audit finds, you have already decided the audit is theater.
Check for the classic vulnerability classes first
Reentrancy, integer overflow/underflow, unchecked external calls, and access-control gaps still account for a large share of real exploits, despite being well understood. Before anything more exotic, confirm the contract follows checks-effects-interactions ordering and that every privileged function has explicit, tested access control.
Beyond the basic single-function reentrancy check, review for cross-function and cross-contract reentrancy, where an external call in one function lets an attacker re-enter through a different function that shares mutable state — this pattern has caused real losses even in contracts that correctly guarded the obvious entry point. Confirm the contract uses reentrancy guards (OpenZeppelin's `nonReentrant` modifier or an equivalent) on every state-changing function that makes an external call, not just the ones the team identified as 'risky' during a quick read-through. On access control, don't just confirm that `onlyOwner` exists — confirm the ownership and role-management model itself is sound: can ownership be renounced accidentally, is there a two-step ownership transfer to prevent a typo'd address from permanently bricking admin functions, and does the role hierarchy correctly separate operational roles (pausing, parameter updates) from truly catastrophic ones (upgrading logic, withdrawing funds)?
Scrutinize oracle and price-feed dependencies
Contracts that rely on external price feeds are only as secure as those feeds. Verify the contract uses a decentralized oracle with manipulation-resistant aggregation rather than a single on-chain price source that a well-capitalized attacker could move within one transaction.
Specifically, check whether the contract reads spot prices directly from a single DEX liquidity pool — a classic setup for a flash-loan price-manipulation attack, where an attacker borrows a large sum, skews the pool price in one transaction, exploits the contract that trusts that price, and repays the loan in the same block. A time-weighted average price (TWAP) sourced over a meaningful window, or a Chainlink-style decentralized oracle network with multiple independent data providers and deviation thresholds, is the standard mitigation. Also verify the contract has sane fallback behavior if an oracle feed goes stale or returns an out-of-range value — does it revert safely, or does it silently accept a zero or wildly incorrect price and let downstream logic act on it? Staleness checks against the oracle's last-updated timestamp are a small addition that prevents a meaningful category of failure.
Confirm test coverage includes adversarial cases, not just happy paths
High line-coverage numbers can still hide gaps if every test assumes honest actors. Fuzz testing and explicit adversarial test cases — reentrant callers, malicious token contracts, griefing attempts — surface the failure modes that matter most once real money is on the line.
A coverage report showing 95% line coverage tells you almost nothing about resilience if every one of those lines was only ever exercised by a well-behaved caller passing valid inputs. Push the test suite to include malicious ERC-20 and ERC-721 token implementations that revert unexpectedly, return false instead of reverting, charge a transfer fee, or rebase supply — any of these can break a contract that assumes standard, well-behaved token semantics. Include griefing scenarios: can an attacker force a function that should be cheap to become prohibitively expensive for other users, for example by spamming a mapping or array the contract iterates over on-chain? Property-based fuzzing tools like Foundry's fuzzer or Echidna should be configured with real invariants specific to your protocol — total collateral always exceeds total debt, the sum of individual balances always equals total supply — and run for enough iterations that rare edge cases actually surface rather than relying on a handful of example-based unit tests.
Plan for what happens after a finding, and after launch
Decide upgrade and pause mechanisms before launch, not during an incident — and make sure key management for any privileged role (owner, pauser, upgrader) uses multi-sig rather than a single key. A contract that's perfectly secure in code but controlled by one compromised wallet isn't secure at all.
For the multi-sig itself, decide the threshold deliberately — a 3-of-5 or 4-of-7 setup with signers spread across different individuals, devices, and ideally organizations is far more resilient than a 2-of-3 controlled entirely by three employees who sit in the same office. Consider a timelock on any privileged action, so that even a compromised multi-sig cannot execute an upgrade or a large withdrawal instantly; a 24-to-48-hour delay gives your team and the broader community a window to notice and react to a malicious pending transaction before it executes. Document, in advance, exactly who has authority to trigger an emergency pause, what the internal escalation path looks like at 3 a.m. on a weekend, and which communication channels (status page, social media, direct partner notifications) get used first. Incident response plans written after an exploit is already underway are written under panic and are measurably worse than ones drafted calmly months in advance.
Verify upgrade patterns don't introduce new attack surface
Upgradeable contracts solve a real problem — the ability to fix bugs and add functionality without a full migration — but the upgrade mechanism itself is a common source of vulnerabilities if it's implemented carelessly. Confirm the team understands the difference between the transparent proxy pattern and the UUPS (Universal Upgradeable Proxy Standard) pattern, and can explain why they chose one over the other for your specific use case.
Check for storage-layout collisions, a subtle but serious class of bug where an upgraded implementation contract declares its variables in a different order or of a different type than the previous version, causing state to be misread or corrupted after the upgrade. Tools like OpenZeppelin's Upgrades plugin can catch this automatically as part of the deployment pipeline, and a competent team should be running that check on every upgrade, not just eyeballing the diff. Also verify that the initializer function on an upgradeable contract can only be called once — an uninitialized or re-initializable proxy has been the root cause of several real-world exploits where an attacker simply called the initializer themselves and took ownership of a freshly deployed, unprotected proxy.
Review economic and game-theoretic assumptions, not just code correctness
A contract can be free of traditional bugs and still be economically exploitable if its incentive design is wrong. This is especially true for DeFi protocols involving lending, staking, or automated market making, where the 'vulnerability' isn't a coding mistake but a scenario the designers didn't model.
Ask the audit to explicitly cover flash-loan-enabled attack paths — not just price manipulation, but governance-vote manipulation, where an attacker borrows a large token supply, votes on a proposal, and repays the loan within a single transaction or block. Review liquidation mechanics in lending protocols for edge cases: what happens during extreme market volatility when many positions become liquidatable simultaneously and liquidator capacity is exhausted, or when gas prices spike so high that liquidations become economically unprofitable and bad debt accumulates instead? For anything involving token emissions or yield, model out whether the incentive structure can be gamed by an actor who deposits and withdraws within the same block purely to farm rewards without taking on the intended economic risk. These reviews require a different skill set than a standard code audit, and it's worth confirming your audit firm — or an economic-modeling specialist alongside them — actually covers this dimension rather than assuming standard code review catches it.
Confirm the audit report itself meets a real bar
Not all audit reports are equally useful, and it's worth reviewing the report format before you commit to a firm. A serious report categorizes findings by severity (critical, high, medium, low, informational/gas), explains the exploit scenario in enough concrete detail that your engineers can reproduce and verify the fix, and includes a remediation review confirming each fix actually resolves the issue rather than just acknowledging it was addressed.
Be skeptical of reports that are thin on critical or high findings for a genuinely novel or complex protocol — either the protocol is unusually simple, or the review wasn't thorough enough. Ask whether the audit included manual review time from senior engineers or was largely automated tooling output with a summary wrapped around it; automated tools are a necessary first pass, not a substitute for a human who understands your specific business logic. And insist on a public or at least shareable version of the final report before mainnet launch — increasingly, exchanges, institutional counterparties, and sophisticated users expect to see it, and a team that's reluctant to publish a clean audit report is signaling something worth asking about directly.
Give inherited and third-party code the same scrutiny as your own
Very few contracts are written entirely from scratch, and that's usually the right engineering decision — battle-tested libraries like OpenZeppelin's ERC standards, access-control modules, and SafeERC20 wrappers have absorbed years of scrutiny that a custom reimplementation wouldn't get. But 'we used a well-known library' is not the same as 'we audited how we used it,' and the audit checklist needs to treat the integration points explicitly.
Check that library versions are pinned exactly, not floated to a range, since even a well-regarded library can introduce a regression or a behavior change between minor versions that your contract's logic silently depends on. Where the team has forked or modified a library rather than using it unmodified — a common move when a project needs slightly different behavior than the stock implementation provides — that modified code deserves the same line-by-line review as fully custom code, because it no longer benefits from the original library's track record. Extend the same scrutiny to the deployment and build tooling itself: npm dependency supply-chain attacks, where a compromised package in the dependency tree injects malicious code into a deployment script, have caused real losses in this industry, so a locked dependency tree and a reviewed deployment pipeline are part of a genuinely complete audit scope, not an afterthought left to a devops team unrelated to the security review.
Consider formal verification for the components where a bug is unacceptable
Manual review and fuzz testing are strong tools, but for a small set of components — the core pricing formula in an AMM, the interest-rate model in a lending protocol, the accounting logic that determines solvency — the cost of a single overlooked edge case is high enough to justify formal verification: mathematically proving that a piece of code satisfies a precisely specified set of properties, rather than testing that it behaves correctly on a finite set of sampled inputs.
Tools like Certora's prover or the Foundry-integrated symbolic execution tooling let a team specify invariants — 'the sum of all user balances always equals total supply,' 'collateralization ratio can never fall below the liquidation threshold without triggering a liquidation' — and mathematically verify those properties hold across every possible input and state transition, not just the ones a fuzzer happened to generate. Formal verification is more expensive in engineering time than fuzzing and isn't a replacement for it; use it selectively on the small number of functions where correctness is truly non-negotiable, rather than trying to formally verify an entire large codebase, which is rarely a practical use of the additional cost and time it requires.