If you’re a developer shipping Solidity code—especially in DeFi, DeFAI, or on-chain AI agents—security audits are non-negotiable. Yet, many struggle with the gap between automated vulnerability reports and manual review nuances. This smart contract security audit checklist for developers is designed from hands-on experience: it walks you through actionable steps to manually audit Solidity smart contracts, spot reentrancy flaws, and augment static analysis with practical safeguards.
I trust this guide will save you debugging hours and help avoid costly exploits. For automated tooling setup, see the Slither setup guide, and for tool comparison, check Aderyn vs Slither comparison.
Before diving into audits, prepare the environment and gather info:
A typical manual audit requires some TypeScript/JavaScript familiarity, since building tests and executing fuzzers often involve those languages.
What does the contract do? Start with the interface (ABIs) and public functions. Trace the call graph:
public or external.onlyOwner, hasRole modifiers.Draw a simple flowchart or bullet list of state transitions. I find this step crucial—too often audits miss subtle logic bugs because the reviewer only hunts for typical weaknesses.
Run a smart contract vulnerability scanner open source tools to get a baseline. Slither remains popular—run it with --print options to identify:
Example Slither command:
slither ./contracts --json results.json
But never trust scanners blindly. Their coverage is incomplete, and they occasionally raise false positives on complex inheritance or inline assembly.
Try a second tool like Aderyn or Oyente if you have time for cross-validation.
Reentrancy remains the top attack vector. Here’s how I approach detection, step-by-step:
call, send, or transfer or external contract calls.ReentrancyGuard or equivalent.Example snippet with reentrancy flaw:
function withdraw(uint amount) external {
require(balances[msg.sender] >= amount, "Insufficient funds");
(bool success, ) = msg.sender.call{value: amount}(""); // external call
require(success, "Transfer failed");
balances[msg.sender] -= amount; // state change after external call
}
Fix by updating state before external call:
function withdraw(uint amount) external {
require(balances[msg.sender] >= amount, "Insufficient funds");
balances[msg.sender] -= amount; // update state first
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
Watch out for proxy patterns, too—reentrancy can bypass logic if storage gaps re-map incorrectly.
Beyond reentrancy, a solid audit touches on several classic vulnerabilities:
| Vulnerability | What To Look For | Why It Matters |
|---|---|---|
| Integer Overflow/Underflow | Unsafe math usage pre-Solidity ^0.8 or unchecked blocks | Can lead to unexpected contract behavior |
| Unchecked External Calls | Missing proper return-value checks on low-level calls | Can cause silent failures |
| Authorization Bypass | Missing access control/modifiers on sensitive functions | Unauthorized state changes and fund drains |
| Front-running/Tx Ordering | No protection for race conditions in sensitive actions | Funds stolen or unfair UX |
| Uninitialized Storage | Storage variables left uninitialized or re-used | Corrupted data, potential backdoors |
| Delegatecall Injection | User-controlled data in delegatecall params | Arbitrary code execution risks |
Slither and SolidityScan AI vulnerability detection can catch many of these but manual confirmation is non-negotiable.
When you wire up an on-chain AI agent's wallet or deploy MCP server contracts, keep in mind:
What I’ve found: careless wallet management often leads to silent drains, a risk that automated audit tools don’t flag.
Security is ongoing, not one-off. Integrate static and dynamic analysis into your pipeline:
I switched to this approach after losing time chasing avoidable runtime bugs.
Testing with unit tests won’t catch everything. Fuzzing injects randomized data to stress test your contract:
Sample truffle test snippet:
it('should revert on unauthorized call', async () => {
await expectRevert(
contractInstance.sensitiveFunction({ from: unauthorizedAddress }),
'Ownable: caller is not the owner'
);
});
Once your audit is complete, document results clearly for yourself, reviewers, or downstream developers:
I typically maintain an audit report Markdown version with issue hashes and references to facilitate post-deployment monitoring.
Smart contract security audits require discipline more than flashy tools. Combining manual vulnerability logic checks—especially for reentrancy—with static analysis and fuzzing will significantly reduce risk.
To keep your audits efficient:
For further guidance on tool setup and comparisons, try Slither setup guide or Aderyn vs Slither comparison. Interested in audit pipeline automation? Check out Smart contract CI/CD pipeline.
Ready to roll? Get your hands dirty, and keep building secure contracts that can survive adversarial environments.
What about integrating AI for exploit generation? That’s a hot topic — see AI agent smart contract exploit generation for a primer on how automated offensive tooling can complement your audit effort.