Smart Contract Security Audit Checklist for Developers
Introduction
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.
Prerequisites for Manual Solidity Audits
Before diving into audits, prepare the environment and gather info:
- Solidity version: Know the exact compiler version and optimizer settings used to compile the contract (matching mainnet deploys matters for bytecode discrepancies).
- Contract source files and dependencies: Multiple contracts, libraries, and interfaces. Audit composite code.
- Protocol documentation: Read the design docs or specifications if available. Understanding feature intent is key.
- Tooling: Install static analyzers like Slither, Aderyn, and fuzzers such as ItyFuzz. I personally run Slither with custom detectors first to catch quick surface-level issues.
A typical manual audit requires some TypeScript/JavaScript familiarity, since building tests and executing fuzzers often involve those languages.
Step 1: Understand the Contract’s Logic and Intent
What does the contract do? Start with the interface (ABIs) and public functions. Trace the call graph:
- Entrypoints: functions marked
public or external.
- Access controls: check
onlyOwner, hasRole modifiers.
- State variables: which ones control critical state changes?
- Modular flows: identify how upgrades, pauses, emergency controls work.
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.
Step 2: Static Analysis and Vulnerability Scanners
Run a smart contract vulnerability scanner open source tools to get a baseline. Slither remains popular—run it with --print options to identify:
- Reentrancy
- Uninitialized storage
- Unsafe delegatecall usage
- Arithmetic overflow/underflow (though Solidity ^0.8+ has built-in checks)
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.
Step 3: Manual Solidity Reentrancy Vulnerability Detection
Reentrancy remains the top attack vector. Here’s how I approach detection, step-by-step:
- Identify external calls: Functions that invoke
call, send, or transfer or external contract calls.
- Check state updates after external calls: If state is modified after an external call, reentrancy risk is present.
- Look for mutexes or reentrancy guards: Confirm presence and correctness of
ReentrancyGuard or equivalent.
- Review payable fallback/receive: Are these relevant for unexpected attacks?
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.
Step 4: Check for Common Smart Contract Vulnerabilities
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.
Step 5: Security Best Practices for Agent Wallets and Key Management
When you wire up an on-chain AI agent's wallet or deploy MCP server contracts, keep in mind:
- Use session keys with scoped permissions and spending limits rather than exposing long-lived private keys.
- Avoid unlimited ERC-20 approvals on agents; prefer minimal allowance patterns.
- Make sure wallets are stored in hardware wallets or encrypted vaults locally—not plain environment variables.
What I’ve found: careless wallet management often leads to silent drains, a risk that automated audit tools don’t flag.
Step 6: Integration with CI/CD and Automated Audits
Security is ongoing, not one-off. Integrate static and dynamic analysis into your pipeline:
- Run Slither and Automated Solidity Security Audit tools on every PR commit.
- Fail builds if critical severity issues are found but allow warnings to prompt manual review.
- Trigger fuzzers like ItyFuzz in nightly builds to catch edge cases.
I switched to this approach after losing time chasing avoidable runtime bugs.
Step 7: Testing and Fuzzing
Testing with unit tests won’t catch everything. Fuzzing injects randomized data to stress test your contract:
- Tools like ItyFuzz (see ItyFuzz fuzzer comparison) provide property-based testing.
- Target functions that handle external calls, critical state changes, and modifiers.
Sample truffle test snippet:
it('should revert on unauthorized call', async () => {
await expectRevert(
contractInstance.sensitiveFunction({ from: unauthorizedAddress }),
'Ownable: caller is not the owner'
);
});
Step 8: Documenting and Reporting Findings
Once your audit is complete, document results clearly for yourself, reviewers, or downstream developers:
- Categorize vulnerabilities by severity (Critical, High, Medium, Low).
- Include code locations, explanation, and if possible, mitigation steps.
- Track false positives for suppression in future scans.
I typically maintain an audit report Markdown version with issue hashes and references to facilitate post-deployment monitoring.
Summary
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:
- Start with understanding contract logic fully.
- Use static scanners as aids but verify findings yourself.
- Focus on reentrancy and classic vulnerabilities manually.
- Manage agent wallets cautiously with scoped keys.
- Automate audits in CI/CD to catch regressions.
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.