Solidity Audit Pre-Check Skill
Automated security pre-flight for Solidity contracts. Catches common vulnerabilities, validates OpenZeppelin usage, and surfaces gas inefficiencies — before you pay $20k+ for a manual audit.
When to Use
- - Before submitting contracts to a paid auditor (Trail of Bits, Certik, Spearbit, etc.)
- After writing a new contract feature or major refactor
- During internal review of a PR that touches contract code
- When onboarding a new contract codebase to understand risk surface
- Pre-deployment to mainnet or any public testnet
When NOT to Use
- - As a replacement for a manual audit. This is a pre-filter, not a security guarantee.
- For non-Solidity contracts (Rust/Anchor, Cairo, Stylus — use language-specific tools)
- For already-deployed and immutable contracts (nothing actionable)
- For frontend/UI code or off-chain scripts
- As a compliance certification — automated tools have known false negative rates
Toolchain Setup
Install required CLI tools:
CODEBLOCK0
Verify:
slither --version
myth version
forge --version
solhint --version
aderyn --version
Pre-Check Workflow
Step 1 — Inventory the Codebase
CODEBLOCK2
Key questions:
- - How many contracts? How many are deployed vs inherited?
- What OpenZeppelin version? (
lib/openzeppelin-contracts/package.json) - External protocols used? (Uniswap, Aave, Chainlink, etc.)
- Upgradeability pattern? (UUPS, Transparent Proxy, Beacon, none)
Step 2 — Slither Static Analysis
CODEBLOCK3
Severity levels:
| Level | Action |
|---|
| High | Block deployment — fix before audit |
| Medium |
Must address — provide justification if not fixed |
| Low | Fix or document rationale |
| Informational | Style/gas — address in optimization pass |
Common high-severity detectors to watch:
- -
reentrancy-eth — reentrant Ether transfers - INLINECODE2 — ERC20 transfer without return value check
- INLINECODE3 — unrestricted ETH send
- INLINECODE4 — attacker-controlled delegatecall
- INLINECODE5 — selfdestruct reachable by anyone
- INLINECODE6 — tx.origin used for auth
Step 3 — Mythril Symbolic Execution
CODEBLOCK4
Mythril catches:
- - Integer overflow/underflow (pre-0.8.x contracts)
- Reentrancy via symbolic paths
- Delegatecall to user-supplied addresses
- Unprotected selfdestruct
- Dependence on predictable values (block.timestamp, blockhash)
Step 4 — Aderyn AST Analysis
CODEBLOCK5
Aderyn is fast (sub-second on most codebases) and catches:
- - Centralization risk (single owner controls)
- Missing events on state changes
- Unsafe ERC20 operations
- Weak access control patterns
- Incorrect inheritance order
Step 5 — Solhint Linting
CODEBLOCK6
Recommended .solhint.json:
{
"extends": "solhint:recommended",
"rules": {
"avoid-call-value": "error",
"avoid-low-level-calls": "warn",
"avoid-tx-origin": "error",
"check-send-result": "error",
"compiler-version": ["error", "^0.8.20"],
"func-visibility": ["warn", {"ignoreConstructors": true}],
"mark-callable-contracts": "error",
"multiple-sends": "error",
"no-complex-fallback": "error",
"no-inline-assembly": "warn",
"not-rely-on-block-hash": "error",
"not-rely-on-time": "warn",
"reentrancy": "error",
"state-visibility": "error"
}
}
Step 6 — OpenZeppelin Pattern Validation
Manual checklist — verify each applies:
CODEBLOCK8
Access Control:
- - [ ]
Ownable2Step used instead of Ownable (prevents accidental ownership transfer) - [ ]
AccessControl roles defined with keccak256 constants, not inline strings - [ ]
DEFAULT_ADMIN_ROLE not held by an EOA in production (use multisig) - [ ]
renounceRole and revokeRole tested for edge cases
Tokens (ERC20/ERC721):
- - [ ]
SafeERC20.safeTransfer used for all external token transfers - [ ]
_safeMint used in ERC721 (not _mint) - [ ] Callback hooks (
onERC721Received) handled - [ ] Reentrancy guard on any function that calls external contracts after state change
Upgradeability:
- - [ ] Initializers called correctly (no constructor logic in upgradeable contracts)
- [ ]
_disableInitializers() called in implementation constructor - [ ] Storage gaps defined in all base contracts
- [ ] ERC-7201 namespaced storage used (OZ v5+)
- [ ] No storage collisions between proxy and implementation
Pausability:
- - [ ]
Pausable integrated with whenNotPaused on critical functions - [ ]
pause() protected by role (not public) - [ ] Emergency unpause path defined and tested
Step 7 — Gas Optimization Audit
CODEBLOCK9
Common gas optimizations to flag:
| Pattern | Issue | Fix |
|---|
| INLINECODE23 in loop | Wastes gas | INLINECODE24 (zero default) |
| INLINECODE25 variable read in loop |
SLOAD per iteration | Cache in
memory first |
|
require(condition, "long string") | String costs gas | Use custom errors |
| Multiple mappings vs struct | Extra SLOADs | Pack into struct |
|
public on internal functions | Extra ABI overhead |
internal or
private |
| Unbounded loops | DoS risk + gas | Add
maxIterations param |
|
transfer() /
send() | 2300 gas limit | Use
call{value: x}("") |
Custom error pattern (saves ~50 gas per revert):
// Bad
require(balance >= amount, "Insufficient balance");
// Good
error InsufficientBalance(uint256 balance, uint256 required);
if (balance < amount) revert InsufficientBalance(balance, amount);
Step 8 — SWC Registry Cross-Reference
Cross-check findings against SWC Registry:
| SWC | Name | Slither Detector | Severity |
|---|
| SWC-100 | Function Default Visibility | INLINECODE35 , INLINECODE36 | High |
| SWC-101 |
Integer Overflow/Underflow |
integer-overflow | High (pre-0.8.x) |
| SWC-104 | Unchecked Call Return Value |
unchecked-transfer | High |
| SWC-105 | Unprotected Ether Withdrawal |
arbitrary-send-eth | High |
| SWC-106 | Unprotected SELFDESTRUCT |
suicidal | High |
| SWC-107 | Reentrancy |
reentrancy-eth,
reentrancy-no-eth | High/Med |
| SWC-111 | Use of Deprecated Functions |
deprecated-standards | Med |
| SWC-112 | Delegate Call to Untrusted Callee |
controlled-delegatecall | High |
| SWC-115 | Authorization via tx.origin |
tx-origin | High |
| SWC-116 | Block values as time proxy |
weak-prng,
block-timestamp | Low/Med |
| SWC-120 | Weak Sources of Randomness |
weak-prng | High |
| SWC-128 | DoS With Block Gas Limit |
costly-loop | Med |
Step 9 — Manual Review Checklist
Items automated tools miss — must be done by human:
Business Logic:
- - [ ] Economic invariants hold (balances sum to total supply, etc.)
- [ ] State machine transitions are complete and non-overlapping
- [ ] Off-by-one errors in fee/slippage calculations
- [ ] Rounding direction favors protocol, not attacker (round down for user, up for protocol)
- [ ] Flash loan attack surface: any function callable within a single tx that can drain funds?
Access Control Logic:
- - [ ] Every privileged function has a role check
- [ ] Role assignments tested for separation of duties
- [ ] Time locks on critical parameter changes (fee changes, oracle updates)
Oracle / Price Feed:
- - [ ] Chainlink feeds: stale data check (
updatedAt + heartbeat) - [ ] Chainlink feeds: min/max circuit breakers handled
- [ ] TWAP used instead of spot price for large positions
- [ ] No single-source price oracle
Proxy Patterns:
- - [ ] Proxy admin is a multisig, not an EOA
- [ ] Implementation cannot be initialized by attacker
- [ ]
selfdestruct in implementation would brick proxy — is it present?
Step 10 — Pre-Audit Report Template
Generate a structured handoff document for the audit team:
CODEBLOCK11
Example: Full Pre-Check Run
CODEBLOCK12
Integration with Other Skills
- - develop-secure-contracts — Use to integrate OpenZeppelin patterns that this skill validates
- upgrade-solidity-contracts — Pre-check upgradeable proxies before upgrade
- ethskills — General Ethereum dev context and deployment patterns
- erc20-tokenomics-builder — Pre-check token contracts built with this skill
Severity Thresholds for Deployment Go/No-Go
| Severity | Count | Decision |
|---|
| Critical/High | Any | Block — do not deploy |
| Medium |
> 3 |
Block — audit required |
| Medium | 1-3 | Mitigate or justify in writing |
| Low | Any | Document and accept or fix |
| Informational | Any | Fix in optimization pass |
A clean pre-check (zero High/Critical, documented Mediums) typically reduces manual audit time by 30-50% and audit cost by 20-40%.
Solidity 审计预检技能
针对Solidity合约的自动化安全预检。在您支付2万美元以上进行人工审计之前,捕获常见漏洞、验证OpenZeppelin使用情况并发现Gas效率问题。
使用场景
- - 在将合约提交给付费审计机构(Trail of Bits、Certik、Spearbit等)之前
- 编写新合约功能或进行重大重构之后
- 在内部审查涉及合约代码的PR时
- 在接手新合约代码库以了解风险面时
- 在主网或任何公共测试网部署之前
不适用场景
- - 不能替代人工审计。 这是预过滤器,而非安全保证。
- 非Solidity合约(Rust/Anchor、Cairo、Stylus — 请使用特定语言工具)
- 已部署且不可变的合约(无实际可操作项)
- 前端/UI代码或链下脚本
- 作为合规认证 — 自动化工具存在已知的漏报率
工具链配置
安装所需的CLI工具:
bash
Slither — 最全面的静态分析器
pip install slither-analyzer
Mythril — 符号执行 / 基于SMT的分析
pip install mythril
Foundry (forge, cast, anvil) — 用于测试和Gas快照
curl -L https://foundry.paradigm.xyz | bash && foundryup
Solhint — 风格和安全规则检查器
npm install -g solhint
Aderyn — 基于Rust的Solidity AST分析器(快速)
cargo install aderyn
验证:
bash
slither --version
myth version
forge --version
solhint --version
aderyn --version
预检工作流程
第一步 — 盘点代码库
bash
统计合约数量,识别外部依赖
find . -name *.sol | head -50
cat foundry.toml # 或 hardhat.config.ts
cat remappings.txt
识别入口点(已部署合约 vs 库/接口)
grep -r contract src/ --include=*.sol | grep -v interface\|abstract\|library
关键问题:
- - 有多少个合约?其中已部署的和继承的分别有多少?
- 使用哪个OpenZeppelin版本?(lib/openzeppelin-contracts/package.json)
- 使用了哪些外部协议?(Uniswap、Aave、Chainlink等)
- 升级模式?(UUPS、透明代理、信标、无)
第二步 — Slither静态分析
bash
使用所有检测器进行全面扫描
slither . --checklist --markdown-root .
JSON输出用于结构化审查
slither . --json slither-output.json
打印人类可读摘要
slither . --print human-summary
按严重性过滤
slither . --filter-paths lib/,node_modules/ --exclude-low
严重性级别:
必须处理 — 如不修复需提供理由 |
| 低 | 修复或记录理由 |
| 信息 | 风格/Gas — 在优化阶段处理 |
需要关注的高严重性检测器:
- - reentrancy-eth — 可重入的ETH转账
- unchecked-transfer — 未检查返回值的ERC20转账
- arbitrary-send-eth — 不受限制的ETH发送
- controlled-delegatecall — 攻击者控制的delegatecall
- suicidal — 任何人可触发的selfdestruct
- tx-origin — 使用tx.origin进行认证
第三步 — Mythril符号执行
bash
分析单个合约(较慢,更深入)
myth analyze contracts/MyContract.sol --solc-json mythril-config.json
针对大型合约设置超时
myth analyze contracts/MyContract.sol --execution-timeout 120 --max-depth 12
输出为JSON
myth analyze contracts/MyContract.sol -o json
Mythril捕获:
- - 整数溢出/下溢(0.8.x之前的合约)
- 通过符号路径发现的重入
- 对用户提供地址的delegatecall
- 未保护的selfdestruct
- 对可预测值的依赖(block.timestamp、blockhash)
第四步 — Aderyn AST分析
bash
从项目根目录运行
aderyn .
输出Markdown报告
aderyn . --output report.md
针对特定目录
aderyn src/ --output aderyn-report.md
Aderyn速度快(大多数代码库亚秒级完成),捕获:
- - 中心化风险(单一所有者控制)
- 状态变更缺少事件
- 不安全的ERC20操作
- 弱访问控制模式
- 不正确的继承顺序
第五步 — Solhint代码检查
bash
初始化配置
solhint --init
检查所有合约
solhint contracts/
/*.sol
使用安全规则集
solhint --config .solhint.json contracts/
/*.sol
推荐的.solhint.json:
json
{
extends: solhint:recommended,
rules: {
avoid-call-value: error,
avoid-low-level-calls: warn,
avoid-tx-origin: error,
check-send-result: error,
compiler-version: [error, ^0.8.20],
func-visibility: [warn, {ignoreConstructors: true}],
mark-callable-contracts: error,
multiple-sends: error,
no-complex-fallback: error,
no-inline-assembly: warn,
not-rely-on-block-hash: error,
not-rely-on-time: warn,
reentrancy: error,
state-visibility: error
}
}
第六步 — OpenZeppelin模式验证
手动检查清单 — 验证每项是否适用:
bash
检查OZ版本
cat lib/openzeppelin-contracts/package.json | grep version
查找所有继承的OZ合约
grep -r import.
openzeppelin contracts/ --include=.sol
访问控制:
- - [ ] 使用Ownable2Step而非Ownable(防止意外所有权转移)
- [ ] AccessControl角色使用keccak256常量定义,而非内联字符串
- [ ] 生产环境中DEFAULTADMINROLE不由EOA持有(使用多签)
- [ ] 测试renounceRole和revokeRole的边缘情况
代币(ERC20/ERC721):
- - [ ] 所有外部代币转账使用SafeERC20.safeTransfer
- [ ] ERC721中使用safeMint(而非mint)
- [ ] 处理回调钩子(onERC721Received)
- [ ] 在状态变更后调用外部合约的函数上使用重入保护
可升级性:
- - [ ] 正确调用初始化器(可升级合约中无构造函数逻辑)
- [ ] 在实现构造函数中调用_disableInitializers()
- [ ] 所有基础合约中定义存储间隙
- [ ] 使用ERC-7201命名空间存储(OZ v5+)
- [ ] 代理和实现之间无存储冲突
可暂停性:
- - [ ] Pausable与关键函数上的whenNotPaused集成
- [ ] pause()受角色保护(非公开)
- [ ] 定义并测试紧急恢复路径
第七步 — Gas优化审计
bash
Forge Gas快照基线
forge snapshot
每个函数的Gas报告
forge test --gas-report
比较优化前后
forge snapshot --diff .gas-snapshot
需要标记的常见Gas优化:
| 模式 | 问题 | 修复 |
|---|
| 循环中uint256 i = 0 | 浪费Gas | uint256 i(默认值为零) |
| 循环中读取storage变量 |
每次迭代SLOAD | 先缓存到memory |
| require(condition, long string) | 字符串消耗Gas | 使用自定义错误 |
| 多个mapping vs 结构体 | 额外SLOAD | 打包到结构体 |
| 内部函数使用public | 额外ABI开销 | 使用internal或private |
| 无界循环 | DoS风险+Gas | 添加maxIterations参数 |
| transfer() / send() |