👋 Introduction
A smart-contract audit is one of the few engineering activities where the cost of being unprepared is measured in calendar weeks and tens of thousands of dollars. Show up with messy code and unclear invariants, and the auditors spend half their time discovering things you already knew — at $400+/hour.
This post is the checklist we run our own contracts through before any external audit. Done well, it cuts audit findings dramatically, shortens the engagement by days, and (most importantly) prevents the kind of bug that turns into a Twitter thread you do not want to be on.
It is not a substitute for an audit. It is the work that makes the audit worth doing.
Before You Write Code: Establish Invariants
The single highest-leverage thing you can do for security is write the invariants down. An invariant is a property that should always be true: "the sum of all user balances equals the total supply"; "a withdrawal can only be initiated by the owner of the deposit"; "the protocol's collateralization ratio never falls below 110%."
These belong in a document — and ideally as executable property tests (Foundry's invariant testing or Echidna). Without explicit invariants, neither you nor your auditors can confidently say "the protocol works." With them, every fuzz run, every test, and every audit checks the same well-defined contract.
If you cannot list the top 10 invariants of your protocol on a single page, stop and write them. Then come back.
The Solidity Checklist
What follows is the checklist we apply to every contract before submission. Treat it as a baseline, not an upper bound.
1. Re-entrancy
Re-entrancy is no longer the most common bug, but it is still the most expensive when it happens.
- Apply the checks-effects-interactions pattern in every external-call site
- Use OpenZeppelin's
ReentrancyGuardon functions that make external calls - Be especially careful with ERC-777, ERC-1155, and any ERC-20 that has
transferhooks - Consider whether read-only re-entrancy is exploitable (a function called via
staticcallthat reads state mid-update)
2. Access control
The boring bug that kills more protocols than anything clever.
- Every
externalandpublicfunction has explicit access control or an explicit comment that it is intentionally permissionless - Use a battle-tested library —
Ownable,AccessControl, or your governance contract — not custom role checks - Two-step ownership transfer (
Ownable2Step) for any role that controls funds - A documented inventory of every privileged function and who can call it
3. Arithmetic and accounting
- Solidity ≥0.8 gives you overflow checks, but
uncheckedblocks bypass them. Audit everyuncheckedfor safety. - Use fixed-point libraries (
PRBMath,solmate FixedPointMathLib) for any math involving fractions, not hand-rolledmulDivchains - Round in the protocol's favor on every conversion. Always.
- Check for division-before-multiplication that loses precision
- Confirm every external token has the decimals you assumed (USDT is 6, not 18 — many bugs start there)
4. External calls and oracles
- Every external call assumes the call can fail and the callee is malicious
- For ERC-20 transfers, use
SafeERC20— some tokens returnfalse, some return nothing, some revert - For oracle reads (Chainlink and similar), check staleness (
updatedAt) and roundId completeness - Use TWAP rather than spot prices for any value used in liquidations or pricing
- Have a fallback for oracle failure, even if it is just "pause the protocol"
5. Front-running and MEV
- Any function that sets a price, redeems an asset, or executes a trade is front-runnable
- Use commit-reveal schemes for sensitive on-chain operations
- Consider deadlines and slippage parameters on every swap-like function (and require them, not default them)
- Be aware of sandwich attacks on any LP-style interaction
- For governance, consider timelocks on every privileged action
6. Initialization and upgrades
- For upgradeable contracts, the implementation contract's initializer must be called by the proxy and the implementation itself must be initialized to disable direct use (
_disableInitializers) - All storage slots are documented and never reordered between upgrades
- Use
gaparrays in upgradeable inheritance hierarchies - Storage layout is verified (Foundry's
forge inspect storage-layout) before every upgrade
7. Gas and DoS
- Loops over user-controlled arrays are bounded or paginated
- No
payablefunction relies ontransferorsend— usecallwith proper return-value handling - Functions that depend on third-party callbacks have gas grief mitigations
- Heavy functions stay below the practical block-gas budget for the chain you target
8. Code hygiene that auditors care about
- No commented-out code in the final commit
- No
TODOorFIXMEleft in production paths - Consistent use of
requirestrings or custom errors (custom errors save gas; pick one and stick to it) - NatSpec comments on every external function
- A
versionconstant or function so auditors can confirm which artifact they are reading
Tooling You Should Be Running on Every Commit
You should not be paying auditors to find issues that a free tool would have caught. The current floor:
- Slither — static analyzer with deep Solidity-specific checks. Run on every PR; fail the build on new high/medium findings.
- Foundry — your test framework, but more importantly your fuzz and invariant runner. Aim for >95% line coverage and at least one invariant test per protocol-level property.
- Echidna or Halmos — symbolic and property-based testing. Catches edge cases fuzzers miss.
- Mythril or Manticore — symbolic execution for deeper analysis on critical paths.
- forge-coverage — visibility into what your tests do not cover.
Set these up in CI before you write the first audit-targeted line. The contracts that arrive at audit with green CI on all of these get measurably fewer findings.
The Test Suite Auditors Want to See
Auditors love a test suite that does the work for them. The structure that consistently impresses:
- Unit tests for every function, including obvious failure modes
- Integration tests for every cross-contract flow
- Fork tests against mainnet for any integration with live protocols (DEXes, oracles, lending markets)
- Fuzz tests for every function with non-trivial inputs
- Invariant tests that run for hours and try to break your stated invariants
- A "scenarios" suite that walks through real user journeys end-to-end
If your test suite covers all six, you are ahead of the median protocol that goes to audit — and that means a faster, cheaper, deeper review.
A Note on Audit Selection
Not all auditors are equal, and the choice matters more than the price. Things that correlate with a good engagement:
- A track record on protocols similar to yours (DeFi, NFT, infra, RWA — they are different specialities)
- A willingness to engage with your invariants and threat model, not just look at code
- A clear report format — severity, root cause, remediation, response — not just a bug list
- Two engineers minimum on the engagement; lone auditors miss things
Whoever you hire, give them context: the design doc, the threat model, the invariants, the in-scope and out-of-scope contracts. Auditors who walk in cold spend the first half of the engagement coming up to speed.
What To Do With the Findings
Every finding is a conversation, not a verdict. Some of the most productive audit interactions happen during the response phase:
- Acknowledge or contest each finding with a clear technical argument
- Re-test against the auditor's PoC after every fix
- Re-run the full tooling suite after every batch of fixes — a fix in one contract often changes behavior in another
- Get a follow-up review of your remediations, not just a sign-off email
The audit report is a public artifact. The thoughtfulness of your responses is what your users will remember.
Conclusion
Smart-contract security is unforgiving. The bugs are public, the losses are immediate, and the protocols that ship without doing the homework end up as case studies. The good news: most production-grade security is grunt work — explicit invariants, rigorous tests, boring tooling — and any team can do it.
Use the checklist above as a starting point. Adapt it to your protocol. Add to it as you learn. And run it before you spend the audit budget — the auditors will thank you, and so will your users.
Need a Pre-Audit Sweep?
We work with teams ahead of their formal audits — running the tooling, writing the invariant tests, and surfacing the issues that would otherwise show up in someone else's report. If you want to walk into your audit with confidence, book a call.
Thanks for reading 😄.
