Pre-launch smart contract checklist
Before you deploy, check five things: who can call every admin function, that the math rounds the way you expect, that state changes before any external call, that signatures can't be faked or reused, and that it all passes fuzz and invariant tests on a mainnet fork. The full list below has 40 checks in 8 areas, based on what we check across 700+ deployed contracts.
Three bugs that pass your tests
Tests check what you expected to happen. These bugs live in what you didn't expect, and each one fits in a single line.
No signature check
A function your app is meant to call, but anyone can call it and award themselves points.
- function addPoints(address user, uint pts) external {
+ function addPoints(address user, uint pts, bytes calldata sig) external {
+ require(verify(user, pts, sig), "not signed by app");
Dividing before multiplying
Integer division rounds down first, so the percentage comes out as 0 for almost every player.
- uint pct = score / maxScore * 100; // 0 unless score >= maxScore
+ uint pct = score * 100 / maxScore;
Calling out before updating state
The receiver can call back in before ownership is recorded, and act on the old state.
- IReceiver(to).onReceive(id);
- owner[id] = to;
+ owner[id] = to;
+ IReceiver(to).onReceive(id);
40 checks before mainnet
Written for Solidity contracts on EVM chains such as Ethereum, BNB Chain, Base, Arbitrum and Polygon. Go through it once the code is frozen, then again after your testnet dry run.
Access control
Many of the worst exploits come down to someone calling a function they shouldn't be able to.
- Every state-changing function has the right access checkList every
publicandexternalfunction that changes state. Each one should be open to everyone on purpose, or protected byonlyOwneror a role. - Ownership sits in a multisigMove ownership to a multisig before launch, not a single deployer wallet whose key sits on one laptop.
- Ownership transfers take two stepsUse
Ownable2Stepor similar, so ownership can't be sent to a mistyped address. - Initializers can only run onceIn upgradeable contracts, protect
initialize, and call_disableInitializers()in the implementation's constructor. - Every admin power is written downList what the owner can do (mint, pause, change fees, upgrade, withdraw) and who holds each key. Holders will ask, so publish it.
Math and logic
Solidity has no decimals, and integer division rounds down. Small rounding mistakes become real losses at scale.
- Multiply before you divide
score / maxScore * 100is 0 unless the score reaches the max. Writescore * 100 / maxScore. - Rounding works in the protocol's favorWhen splitting rewards or shares, round down what users receive, and make sure leftover dust can't be drained or block withdrawals.
- Token decimals are read, not assumedStablecoins often use 6 decimals on Ethereum and 18 on BNB Chain. Read
decimals()instead of hardcoding1e18. - Edge cases are testedZero amounts, the first and last user, maximum values, and an empty pool or reward list.
- Every
uncheckedblock is justifiedSolidity 0.8 reverts on overflow, except insideunchecked. Each one needs a clear reason it can't overflow.
External calls
Any call to another contract hands control to code you didn't write.
- State changes before external callsUpdate balances and ownership first, then call out or send ETH (checks-effects-interactions). Calling out first is how reentrancy happens.
- A reentrancy guard where funds moveAdd
nonReentrantto functions that send ETH or tokens, or that call an address the user supplies. - Token transfers use SafeERC20Some tokens, like USDT on Ethereum, don't return a bool.
safeTransferhandles them and reverts on failure. - Received amounts are measured, not assumedFee-on-transfer tokens deliver less than the amount sent. If you accept any token, compare the balance before and after.
- Prices can't be moved in one transactionNever price off a single DEX pool's current reserves, because a flash loan can move them. Use a TWAP or an oracle like Chainlink, and check how fresh the price is.
Signatures
If your app signs messages for the contract to accept, the contract has to check them properly.
- The signer is checkedRecover the signer and compare it to your app's signing address. A function like
addPoints(user, pts)with no signature lets anyone award themselves points. - Each signature can only be used onceInclude a nonce, or mark each signature as used, so the same one can't be submitted twice.
- Signatures expireInclude a deadline in the signed data and reject anything past it.
- Signatures only work on this contract and chainUse EIP-712 with the chain ID and contract address in the domain, so a testnet signature, or one meant for another contract, fails here.
- Invalid signatures revertRaw
ecrecoverreturnsaddress(0)for a bad signature, which passes if the expected signer is ever unset. OpenZeppelin's ECDSA library reverts instead.
Token settings
For token launches. Holders and token scanners read these settings before they buy.
- Supply and allocations match what you announcedCheck minted amounts, decimals and every wallet allocation against your published tokenomics.
- Fees and limits have hard capsIf the owner can change buy and sell fees or the max wallet size, cap them in the contract so they can never be set to 100%.
- Trading can't be switched off againOnce trading is enabled, there should be no way to disable it. A switch that can stop sells looks like a honeypot to holders and scanners.
- No hidden mint, blacklist or fee exemptionsScanners will find any mint, blacklist or fee-exempt function. Remove it, or explain publicly why it's there.
- The liquidity plan is in the launch scriptDecide whether LP tokens are locked or burned, and do it in the script so it isn't forgotten on launch day.
Testing
Coverage shows which lines ran, not whether they're right. Test what must never happen, not just what should.
- Unit tests for every function and every revertTest the failure paths, not just the happy path.
- Fuzz tests on inputsLet Foundry try thousands of random amounts and addresses, for example
testFuzz_strangerCannotBecomeOwner(address). - Invariant tests for what must always be trueFor example: total supply never exceeds the cap, and the contract always holds enough to pay every claim.
- A fork test against mainnetRun the launch on a fork of the real chain, with the real router, tokens and oracles.
- Coverage reviewed, not just measured
forge coverageshows lines that never ran. Read what's missing instead of chasing 100%.
Deployment
Most launch-day problems are settings, not code.
- A testnet dry run with the real scriptDeploy with the exact script and settings you'll use on mainnet, then click through the whole app.
- Constructor arguments checked twiceOwner, token addresses, router, fees and start times. A wrong address here usually means redeploying.
- Source code verified on the block explorerUse
forge create --verifyorforge verify-contract. Holders treat unverified contracts as a red flag. - Compiler version and settings pinnedUse a fixed
pragmaand the same optimizer settings you tested with. - The deployer key is safeDeploy from a fresh wallet or a hardware wallet, and never put a private key in a file that gets committed.
After launch
Plan for problems before you have one.
- Monitoring and alerts are onGet alerts for large transfers, admin calls and failed transactions, so you hear about problems before your holders do.
- There's a pause switch, and you know who holds itFor contracts that hold funds, a
pausefunction on a multisig buys time during an incident. - Admin changes go through a timelockFor contracts holding user funds, a delay on upgrades and setting changes gives holders time to react.
- An incident plan is written downWho gets called, how you pause and what you post publicly. Decide it before you need it.
- Official addresses are published in one placeList every contract address on your site and socials so holders can spot fakes.
Common questions
Does this checklist replace an audit?
No. It covers common mistakes, but contracts that will hold significant user funds should also get an independent audit.
When should I go through it?
Once the code is frozen, and again after the testnet dry run with your real deployment script. Leave time before launch day to fix what you find.
Does it apply to Solana programs?
Partly. The access control, signature, testing and deployment checks carry over, but Solana programs have their own risks, like checking that every account passed in is the one you expect.
Can you review our contracts against it?
Yes. Send us your contracts through the contact form and we'll go through them against this checklist before you deploy.
Want a second pair of eyes?
Send us your contracts and we'll go through them against this checklist before you deploy.
