The Pre-Launch Smart Contract Security Checklist
A pre-launch checklist for Solidity contracts: access control, arithmetic, reentrancy, oracles, invariants, upgrades, and testing. Run it before you book an audit.
By Alex Cipher
This is the checklist I run against a codebase before an audit begins, published so you can run it yourself first. Everything you fix in advance is time the review spends on harder problems instead — which means a better audit for the same money.
1. Access control
- List every privileged function and who can call it. If that list is longer than you expected, that is already a finding.
- Confirm every admin function has an access modifier. Missing modifiers on a single function is a classic total-loss bug.
- Check the constructor and initialiser. On upgradeable contracts, confirm the initialiser cannot be called twice and cannot be front-run on deployment.
- Decide what happens if the admin key is lost or compromised — and whether a timelock or multisig should stand between an admin action and its effect.
- Verify that pause functions cannot trap user funds permanently. A pause that blocks withdrawals is a rug with extra steps, whether or not you intend it that way.
2. Accounting and arithmetic
- For every division, know which way it rounds and who benefits. Rounding must never favour the user at the protocol's expense in a function that can be called repeatedly.
- Check the order of operations: multiply before dividing, or accept precision loss you have consciously decided is acceptable.
- Audit every
uncheckedblock and justify it. Solidity 0.8's default checks exist for a reason. - Confirm that any value tracked in two places — an internal balance and the token balance, say — cannot drift apart.
- Test with realistic decimals. Code that works with 18-decimal tokens frequently breaks with USDC's 6.
3. External calls and reentrancy
- Map every external call. For each, ask what happens if the callee reverts, consumes all gas, or calls back into you.
- Apply checks-effects-interactions consistently. Update state before you call out, every time.
- Consider cross-function reentrancy, not just the same function. A guard on
withdrawdoes nothing if the callback enterstransfer. - Check return values on low-level calls. A silently failing
.callis worse than a revert. - Handle non-standard ERC-20s: tokens that return no boolean, take a fee on transfer, or rebase.
If your protocol lets anyone add an arbitrary token — a pool, a listing, a reward asset — assume a malicious token will be added. It can reenter, revert selectively, or lie about balances.
4. Oracles and price data
- Confirm you are not reading a spot price that a flash loan can move within the same block.
- Handle stale data explicitly. Check the update timestamp and decide what the protocol does when a feed goes quiet.
- Handle implausible values — zero, negative, or wildly out of range — rather than assuming they cannot occur.
- Know what happens if the oracle is unavailable entirely. Pausing is a legitimate answer; silently using a default is not.
5. Economic and game-theoretic checks
- Write down your invariants: total deposits equal the sum of balances, rewards distributed never exceed rewards funded, and so on. Then test them under fuzzing.
- Ask whether anyone profits from calling your functions in an unusual order, or in the same block.
- Check for first-depositor and empty-pool edge cases — a persistently fertile source of vault exploits.
- Model what a large actor can do: what does someone with 51% of the supply, or a flash loan of any size, get out of your system?
- Consider MEV. If an action becomes profitable once it is visible in the mempool, someone will act on it.
6. Upgradeability and deployment
- If using proxies, verify storage layout compatibility and never reorder existing variables.
- Confirm the implementation contract cannot be initialised directly by an attacker.
- Review the deployment scripts as carefully as the contracts. Shipping a different bytecode than the one reviewed is not a rare failure.
- Verify constructor arguments and post-deploy configuration on a testnet fork before mainnet.
- Publish and verify source on the block explorer. Unverified contracts erode trust for no benefit.
7. Testing
- Aim for high branch coverage, not just line coverage — the untested branch is where the bug lives.
- Add fuzz tests for anything arithmetic. Foundry makes this cheap and it finds real bugs.
- Write invariant tests for the properties from section 5.
- Fork-test against mainnet state if you integrate live protocols. Mocks agree with your assumptions; reality does not.
- Write at least one test that tries to steal the funds. Thinking like the attacker for an hour is unreasonably effective.
8. Before you hand it over
- Freeze the code. Give the auditor a specific commit. Reviewing a moving branch wastes their time and your money.
- Write down what it is supposed to do. Even a page of plain prose about intended behaviour and trust assumptions dramatically improves what a reviewer can find.
- Say what worries you. Teams almost always have an instinct about the sketchiest part of their system. Share it.
- List your assumptions. “We assume the oracle is honest” is fine — as long as it is stated, so someone can push back on it.
Running this list yourself is not a substitute for review — you cannot audit your own assumptions, which is the whole reason external review exists. It just means the review starts from a much better place.
When you are ready, the security review pricing page lists what each tier covers, and this piece on audit cost explains what drives the number.
Keep reading
How Much Does a Smart Contract Audit Cost?
Real market rates for independent auditors, boutique firms, and top-tier firms — and the variables that actually move the number.
AuditingHow to Read a Smart Contract Audit Report
Scope sections, severity ratings, and why 'acknowledged' means the bug is still there. How to judge an audit report instead of skimming the summary table.