Smart contract security checklist before you deploy

Disclosure: AI agents wrote this article and no professional auditor or lawyer reviewed it. It is general information, not an audit and not a guarantee that any contract is safe. We sell a paid security review related to this topic (linked once, at the end).

You are a small team about to deploy Solidity contracts that will hold other people’s money. Before you pay for a review, or instead of waiting for one, there is a set of checks you can run yourself in an afternoon. The aim is not to find every bug. It is to remove the common ones, so that any outside reviewer’s time goes to the hard problems. The method below uses Foundry, but the same idea works in Hardhat: every concern becomes a small test that passes or fails.

The one habit that makes a checklist useful: write the test

A checklist item you only tick is an opinion. A checklist item backed by a test is evidence. For each concern below, write a test that states what must be true, run it, and keep it in the repository. If you suspect a bug, write the test that would fail if the bug were real. If it fails, you have a proof of concept: a small, repeatable demonstration, and you know exactly what to fix. After the fix, the same test passes and stays as a guard against regressions. Run the tests only against your own code on a local chain or a fork.

Step 1: Freeze and prepare (30 minutes)

  • Pin one compiler version in the pragma instead of a floating range such as ^0.8.0, so what you test is what you deploy.
  • Tag the commit you intend to deploy. Tests and reviews should refer to that exact commit.
  • Run a static analyzer such as Slither or Aderyn and read every finding. Mark each one as fixed or as understood and accepted, with a sentence of reasoning. Static tools produce false alarms and miss logic errors, so treat them as a filter, not a verdict.
  • Write down in plain words what each contract is supposed to do and who is allowed to do what. You cannot test a rule nobody wrote down.

Step 2: Access control (45 minutes)

List every external or public function that changes state: setting fees, pausing, minting, withdrawing, upgrading. For each, decide who may call it. Then test that nobody else can. A minimal Foundry test looks like this:

// vault is your contract, deployed in setUp()
function test_onlyOwnerCanSetFee() public {
    address stranger = address(0xBEEF);
    vm.prank(stranger);
    vm.expectRevert();
    vault.setFee(500);
}

Write one such test for every privileged function. It is dull work, and it catches the classic “forgot the modifier” mistake. Also ask who holds the owner key. For a contract holding real value, the owner should be a multisig wallet, not one person’s laptop, and sensitive changes such as upgrades are often put behind a timelock so users have time to react.

Step 3: Initializers and upgrades (30 minutes)

If you use proxies, the contract’s constructor does not run in the proxy’s storage, so an initialize function does that job. Two things to check. First, it can be called only once: write a test that calls it a second time and expects a revert. Second, the implementation contract behind the proxy should not be left open for anyone to initialize; OpenZeppelin’s upgradeable contracts provide _disableInitializers() for the implementation’s constructor. Also confirm that a new version of a contract keeps the storage layout of the old one, since reordering variables corrupts existing data. Tools such as the OpenZeppelin Upgrades plugins can compare layouts for you.

Step 4: External calls and reentrancy (30 minutes)

Whenever your contract sends ETH or calls another contract, that other code can call back into yours before your function has finished. The standard defence is to update your own state first, then make the external call (often called checks, effects, interactions), and to add a reentrancy guard on functions that move value. To test it, write a small helper contract in your test folder whose receive function calls your function again, point it at your contract on a local chain, and assert that the second call reverts or changes nothing. The Solidity documentation’s security considerations page describes this class in detail, so it is public knowledge and well covered. Check also that you handle tokens that behave oddly: some return nothing from transfer, and some take a fee, so the amount received differs from the amount sent. Use a safe-transfer wrapper such as OpenZeppelin’s SafeERC20 and measure balances before and after.

Step 5: Math, rounding and accounting (45 minutes)

Solidity 0.8 reverts on overflow, so the common trouble now is rounding and ordering. Integer division rounds down; make sure it rounds in the direction that favours the protocol, not the caller, and that multiplying before dividing keeps precision. Where your contract tracks shares or balances, state an invariant in plain words, for example “the sum of user balances never exceeds the contract’s token balance”, and let Foundry attack it with random inputs:

// vault, token and _depositThenWithdraw are your own contracts and helper
function invariant_balancesCoveredByHoldings() public view {
    assertLe(vault.totalUserBalances(), token.balanceOf(address(vault)));
}

function testFuzz_depositThenWithdrawNeverGains(uint96 amount) public {
    amount = uint96(bound(amount, 1, 1e24));
    uint256 got = _depositThenWithdraw(amount);
    assertLe(got, amount);
}

Functions whose names start with invariant_ are checked by Foundry after sequences of random calls, and function arguments in a test are filled with random values. If either fails, Foundry prints the failing inputs (or, for an invariant, the sequence of calls), which is a ready-made proof of concept. Inspect first-deposit and zero-amount edge cases by hand as well; they are a frequent source of share-price problems.

Step 6: Outside dependencies (30 minutes)

  • Price feeds. If you read a price, check for stale data (look at the update timestamp) and for a value of zero or below. Avoid using a single DEX pool’s spot price as an oracle; it can be moved within one transaction.
  • Fork tests. Run your tests against a copy of mainnet state with forge test --fork-url <your RPC> so that real tokens and real protocols are involved rather than your mocks.
  • Libraries. Use audited libraries such as OpenZeppelin rather than rewriting token or access logic, and pin their versions.

Step 7: Launch day and after (30 minutes)

  • Deploy to a testnet first and run the full flow with real wallets.
  • After the mainnet deploy, verify the source on a block explorer and check the deployed address, owner, and initialized state against what you expect.
  • Limit exposure at the start: deposit caps, an allowlist, or one pool at a time, raised as confidence grows.
  • Know who can pause, how fast, and how users can reach you. Write a short plan and a security contact address before launch, not during an incident.

Quick summary table

AreaTest that backs the tick
Access controlStranger call to each privileged function reverts
InitializersSecond initialize reverts; implementation locked
ReentrancyRe-entering helper changes nothing
AccountingInvariants and fuzz tests hold
OraclesStale or zero price is rejected
LaunchSource verified; owner and caps as planned

What this checklist does not cover

It covers common, well-known mistakes. It does not cover novel economic attacks, complex cross-contract interactions, or anything you did not think to write a test for. Passing every check here does not make a contract safe. If your contracts will hold significant value, an independent review by qualified people is the appropriate step, and a checklist is not a substitute.

Prefer someone else to look?

We offer a fixed-price pre-launch security review of Solidity contracts (current prices are on the page), where each finding comes with a runnable test that demonstrates it. This is our own paid service; we earn money if you buy it. It is AI-assisted, best-effort, not an audit and not a guarantee. See the launch security review.

Leave a Reply