Why contract security is different
Deployed contracts are public, often immutable and frequently hold value directly. An attacker can read your code, simulate attacks for free and act the moment a weakness appears. Unlike a web application, you may not be able to patch quickly. That makes the work before deployment the main line of defence.
This checklist covers widely documented problem areas for EVM contracts written in Solidity. It is a starting point for your own review process. It does not replace an independent audit, which we recommend for any contract that will hold meaningful value.
Design: keep it small and boring
- Minimise what lives on-chain. Every extra function is extra attack surface.
- Prefer well-reviewed libraries, such as OpenZeppelin, over custom implementations of tokens, access control and math.
- Write down the intended invariants: for example, total shares always equals the sum of balances. Tests and reviews can then check them.
- Decide upfront what happens in an emergency: can the contract be paused, and who can do it?
- Document every privileged role and why it needs the power it has.
Common vulnerability classes to check
Review each function against the following well-known issues.
- Reentrancy: an external call lets another contract call back before your state is updated. Update state before external calls (checks-effects-interactions) and consider a reentrancy guard.
- Access control: missing or wrong modifiers on privileged functions, or an initialiser that anyone can call.
- Integer and precision errors: modern Solidity reverts on overflow by default, but unchecked blocks, casting and rounding in division can still lose or create value.
- Unchecked external calls: low-level calls return a success flag that must be checked; some tokens do not return booleans as expected.
- Oracle and price manipulation: using a spot price from a single liquidity pool can be manipulated within one transaction.
- Front-running and transaction ordering: anything that depends on order, such as auctions or swaps, needs slippage limits or commit-reveal schemes.
- Denial of service: unbounded loops over user-controlled arrays, or logic that depends on an external call succeeding.
- Signature replay: signed messages should include a nonce, the chain id and the contract address.
Upgrades, admin keys and privileged roles
Many real incidents come from operations rather than code. If your contracts are upgradeable, the upgrade key is as valuable as the funds. Put privileged roles behind a multisignature wallet with signers on separate devices, and consider a timelock so users can see changes before they take effect.
- Never use a single externally-owned account as owner of a contract holding value.
- Check storage layout compatibility on every upgrade and protect initialisers.
- Keep deployer keys off shared machines and rotate or renounce them once deployment is done.
- Test the emergency procedures you plan to rely on, including pausing.
Testing and tooling
Unit tests that only cover expected behaviour are not enough. Write tests for abuse: calling functions in the wrong order, with zero or huge values, from the wrong account and from a malicious contract.
- Aim for high branch coverage and review which branches are not covered, rather than chasing a number.
- Add fuzz tests and invariant tests (tools such as Foundry support both) to explore inputs you would not think of.
- Run static analysers such as Slither and treat each finding as something to fix or consciously justify.
- Test against a fork of the target network to exercise real integrations.
- Review compiler warnings and pin the compiler version used for deployment.
Automated tools find known patterns; they do not understand your business logic. Human review of the design is still required.
Review, audit and launch
- Freeze the code and have someone who did not write it review it against the checklist above.
- Fix the findings, add a regression test for each and re-review the changes.
- Commission an independent audit when the contract will hold meaningful value. Give auditors documentation of intended behaviour and invariants.
- Deploy first to a public testnet and rehearse the full launch and incident process.
- Consider a staged launch with deposit caps and a bug-bounty programme.
- Verify the source code on a block explorer so users can inspect what is deployed.
Even after an audit, risk remains. Monitor the contracts on-chain, have an incident plan with named responsibilities, and be honest with users about what has and has not been reviewed.
In short
Keep contracts small, use proven libraries, protect privileged keys with multisignature controls, test adversarially and get independent eyes on the code before it holds value. Security is a process that continues after launch, not a box ticked once.
