W3

智能合约审计检查清单

33 项检查,分 10 组。和绝大部分「审计清单」不同的是, 每一项旁边都写着「这一项工具能不能帮你查」——其中 21 项是任何静态工具都查不出来的。 把这件事标出来,比假装覆盖更有用。

检查项 33分组 10致命权重 14扫描完整覆盖 6工具查不出 21
检查项
33
分 10 组,覆盖权限到代币经济
致命项
14
没通过通常意味着资金可被直接拿走
工具可自动查
6
本站源码扫描有对应规则,会给出证据行号
只能人工查
21
语法完全正常,危险来自运行环境
检查进度0%0 / 33 项已给结论
通过0发现问题0不适用0未检查33
仅完成 0% 的检查,不足以得出结论
风险敞口 0(未通过项按严重程度累加,用来排优先级)。 这不是安全评分——检查项全部通过也不代表合约安全。
还有 14 个致命项没有结论:「Owner 地址是谁?是单一私钥,还是多签?」、「把所有带 owner / admin 修饰器的函数列出来,逐个看它最坏能做什么。」、「mint 函数有没有权限控制?是 external 还是 internal?」、「税率是常量还是可改?可改的话上限是多少?」 等
有 21 个 「工具完全查不出」的项仍是未检查状态。这些项不查,报告里的完整度就是虚的。

勾选记录只存在你这台机器的浏览器里(localStorage),不会上传。 关闭页面后再打开仍在;点「重置」才会清空。

致命

Owner 地址是谁?是单一私钥,还是多签?

只能人工检查
为什么问:单人私钥意味着一个人就能动用全部特权。多签把「作恶」变成需要多人合谋,是最有效的治理约束。
看哪里:链上读 owner() 返回值,到区块浏览器查该地址的历史交易与是否为多签合约(如 Safe)。
工具边界:工具不知道多签阈值和签名人构成,只能告诉你「有 owner」,判断不了这个 owner 是否可信。
高危

特权操作是否有时间锁?改动前用户能否提前看到?

工具能提示,判断需人工
为什么问:没有时间锁,Owner 可以在同一个区块里改税率并撤池。有时间锁,用户至少有机会先撤出。
看哪里:看特权函数的实现里有没有 delay / queue / execute 三段式;很多项目用 OpenZeppelin TimelockController。
工具边界:能看出合约引入了 Timelock 相关依赖,但判断不了 delay 设的是 2 天还是 2 秒——这个参数才决定有没有用。(原理见代理存储冲突与未保护升级)
高危

有没有 renounceOwnership?Owner 是否已经调用过?

工具能提示,判断需人工
为什么问:「没有放弃权限的能力」和「还没放弃权限」是两回事,但风险上都等于「权限仍然存在」。必须分开确认。
看哪里:源码里搜 renounceOwnership;再链上读 owner() 是否已变成 0x000...0。
工具边界:能确认「源码里有没有这个函数」,但是否已调用是链上状态,必须查 owner() 当前值。(原理见访问控制缺失、蜜罐代币)
致命

把所有带 owner / admin 修饰器的函数列出来,逐个看它最坏能做什么。

工具可自动检查
为什么问:不要问「有没有特权函数」(几乎都有),要问「最坏那一个能造成什么损失」。提走资金和改个参数是两回事。
看哪里:用本站的合约体检功能拉函数表,或搜 onlyOwner / onlyRole / require(msg.sender ==。
工具边界:能列出特权函数并给出声明处行号。判断「它最坏能做什么」仍需人读函数体。(原理见访问控制缺失、访问控制缺失、访问控制缺失、蜜罐代币)
致命

mint 函数有没有权限控制?是 external 还是 internal?

工具可自动检查
为什么问:无权限的 external mint = 任何人可以无限造币,持币者直接归零。这是最 cheap 的攻击。
看哪里:搜 function mint / _mint,检查可见性与修饰器。
工具边界:这一项工具做得比人快且准:会区分「完全无权限」和「owner 保留」两种,风险等级不同。(原理见访问控制缺失、蜜罐代币、访问控制缺失)
高危

总供应有硬上限吗?上限是常量还是可改的变量?

只能人工检查
为什么问:有 mint 权限但有 cap,风险是「增发到上限为止」;cap 本身可改,风险就等于无上限。
看哪里:搜 MAX_SUPPLY / cap / _totalSupply 的比较语句,确认 cap 是 constant/immutable 还是普通状态变量。
工具边界:能告诉你「有 mint」,但判不了「上限是否真的封死」——这需要读 mint 函数体里的比较逻辑。
高危

有没有 burn 函数?谁能对「别人的」余额调用 burn?

只能人工检查
为什么问:如果 owner 能 burn 任意地址的余额,那它等价于冻结 + 销毁,是比黑名单更隐蔽的控制手段。
看哪里:看 burn / burnFrom 的参数与权限:_burn(msg.sender, ...) 安全,_burn(address, ...) 带 onlyOwner 就危险。
工具边界:现有规则没有专门的 burn 检查。这一步纯人工,请务必逐个看 burn 的调用者是谁。(原理见蜜罐代币)
高危

有没有黑名单 / 白名单机制?谁能改?

工具可自动检查
为什么问:被拉黑的地址无法卖出。项目方承诺「不会用」没有意义——能力留在代码里就是风险。
看哪里:搜 blacklist / _isBot / addToBlacklist / setBots,看设置函数归谁管。
工具边界:正则覆盖常见命名,能给出证据行号。绕过命名的写法(比如叫 _blocked[addr])会漏。(原理见蜜罐代币)
致命

税率是常量还是可改?可改的话上限是多少?

工具能提示,判断需人工
为什么问:宣传「买卖税 3%」,但代码允许改成 100%,那宣传就不成立。关键是有没有硬编码的 cap。
看哪里:搜 setTaxFee / setFees / updateFee,看函数体里有没有 require(newFee <= MAX_FEE)。
工具边界:能确认「税率可改」并分层级(无 setter / setter 有权限 / setter 无权限),但判不了有没有 cap,必须读函数体。(原理见蜜罐代币)
中等

交易开关归谁管?关掉之后用户还能赎回或卖出吗?

工具可自动检查
为什么问:交易开关本身是中性的(防狙击常用),但「关掉后无法卖出」就构成貔貅。要看关闭时是否也冻结了卖出。
看哪里:搜 tradingEnabled / swapEnabled,在 transfer 里看这个变量如何影响卖出方向。
工具边界:能找出开关变量与暂停能力。判断「关闭时卖出席位是否也被堵」需要读 transfer 分支。(原理见蜜罐代币、访问控制缺失、蜜罐代币)
致命

【必做】实际买入一点,然后立刻尝试全部卖出。

只能人工检查
为什么问:这一项任何静态工具都替代不了。蜜罐的判定条件可以藏在 assembly、动态代理、甚至链下签名里,源码上看不出问题,但真的卖不掉。测试网用测试币走一遍,是唯一可靠的方法。
看哪里:在测试网用极小额走完整流程:买入 → 检查实际到账 → 授权 → 全部卖出 → 确认能收到本金。
工具边界:rugscan 有 honeypot-condition 规则,但只能提示源码里存在可疑条件判断,不能替代真实交易。请把这句原话记在心里:源码没问题的蜜罐是存在的。(原理见蜜罐代币)
致命

每个外部调用是否都在状态更新之后?

工具能提示,判断需人工
为什么问:Checks-Effects-Interactions。先转账再扣余额,对方就能在收到转账的回调里重新进入、重复扣一次之前还没扣的余额。
看哪里:逐个看有外部调用(call / transfer / safeTransfer)的函数,确认余额扣减在调用之前。
工具边界:能命中典型的「调用后才更新状态」模式并给出行号。跨函数、跨合约的状态依赖会漏——静态看单合约看不出来。(原理见重入攻击)
高危

重入锁是否覆盖了全部有外部调用的入口?

工具能提示,判断需人工
为什么问:只在部分函数加 nonReentrant,留一个没加的入口就等于没加。锁的价值取决于最弱的那一环。
看哪里:列出所有含外部调用的 external 函数,确认每一个都带 nonReentrant 或有等价的显式检查。
工具边界:能发现「有外部调用但缺少防护」的函数,正是它的用途。但判断「防护是否等价」需要人读。(原理见重入攻击)
高危

低层 call 的返回值检查了吗?

工具可自动检查
为什么问:address.call(...) 失败时不抛异常,只返回 false。不检查返回值,就等于「转账失败但状态已更新」,直接丢钱。
看哪里:搜 .call{ 与 .call(,确认每个都有 require(success) 或显式的 success 判断。
工具边界:这一项工具命中率很高:模式固定(call 后面没有 require)。会误报「故意忽略失败」的合法场景。(原理见重入攻击)
高危

合约是否接收 ERC-777 / ERC-721 / ERC-1155 回调?回调函数里做了什么?

只能人工检查
为什么问:这些标准的转账会主动调用接收方的 hook(tokensReceived / onERC721Received)。任何「转账完成」的假设在这里都不成立。
看哪里:搜 onERC721Received / tokensReceived / _beforeTokenTransfer。
工具边界:现有规则不覆盖回调。请人工确认回调里有没有改状态、有没有被当成「交易已完成」的信号。
致命

价格从哪里来?用的是现货储备比,还是 TWAP / 外部预言机?

只能人工检查
为什么问:用「池子里的储备量相除」当价格,就等于允许别人在同一笔交易里把池子搬空再还回去——闪电贷让这件事零成本。这是 DeFi 损失最大的一类漏洞。
看哪里:搜 getReserves / balanceOf(pair) / slot0,看结果是否被直接当作价格使用。
工具边界:工具完全查不出来。getReserves() 语法上毫无问题,危险来自「这个值可被瞬间改变」。这一步只能人工判断:价格是否来自单一池子的即时状态。
高危

如果用 TWAP,窗口多长?短窗口照样能被操纵。

只能人工检查
为什么问:TWAP 的防护力等于窗口长度。5 分钟的窗口,攻击者多持有一会儿就能拉平成本,保护有限。
看哪里:搜 observe / consult / twap,看 period 参数的取值。
工具边界:参数长短没有绝对安全值,取决于资产深度。工具给不出判断。
高危

有没有多源交叉验证?偏离阈值如何设定?

只能人工检查
为什么问:单一数据源被操纵或故障时,没有任何东西能拦住错误价格。两个独立来源互相校验是最低要求。
看哪里:看是否同时读取多个预言机或 CEX 报价,以及偏离时是暂停还是取中位数。
工具边界:架构级判断,纯人工。
致命

签名的摘要里包含 chainId 和 verifyingContract 吗?

只能人工检查
为什么问:不含 chainId → 同一个签名能在分叉链或测试网上重放;不含合约地址 → 另一份同样代码的合约也能接受它。EIP-712 的 domain separator 就是为这两件事设计的。
看哪里:搜 DOMAIN_SEPARATOR / EIP712Domain / \x19\x01,确认 domain 里有 chainId 与 verifyingContract。
工具边界:工具查不出来。keccak256(abi.encodePacked(...)) 无论拼了什么参数都语法正确。本站的 EIP-712 工具可以帮你手工验证 domain separator 是否如预期。
致命

有没有 nonce?已用过的签名会被标记吗?

只能人工检查
为什么问:没有 nonce,同一个签名可以在同一条链上被反复提交。凡是「凭签名领钱」的逻辑都必须有这一步。
看哪里:搜 nonce / usedSignatures / _usedHashes,确认使用后立即置位(且置位在外部调用之前)。
工具边界:纯人工。注意检查「置位」与「转账」的先后顺序——顺序错了,nonce 也救不了重入。
高危

ecrecover 是否拒绝了高 s 值?签名是否可被篡改?

只能人工检查
为什么问:同一份签名把 s 取补(并翻转 v)能得到同一个地址,但 bytes 变了。如果合约用签名字节做去重标识,这个签名就能被用两次。
看哪里:搜 ecrecover,确认有 s <= secp256k1n/2 的检查(或直接用 OpenZeppelin ECDSA)。
工具边界:没有对应规则。人工确认是否用了库而非裸写 ecrecover。
高危

除法舍入的方向对谁有利?

只能人工检查
为什么问:整数除法截断。如果「铸造份额时向上取整、销毁时向下取整」,用户就能反复存取套出资金。正确方向是:永远让协议略微占优,不允许用户从舍入中获利。
看哪里:找所有除法,判断它计算的是「用户得到多少」还是「协议应收多少」。
工具边界:工具查不出来:a / b 在任何上下文里都合法。这需要结合业务语义逐处推演。
高危

金库的首笔存款是否存在「捐赠放大份额」的攻击面?

只能人工检查
为什么问:合约初始份额为 0 时,攻击者存 1 wei 再直接向合约转账,可把单份价格抬到极高,让后续存款人的钱被舍入成 0 份额。
看哪里:看首次铸造是否锁定了最小流动性(如 OpenZeppelin ERC4626 的 10**decimalsOffset),或是否禁止直接转账入金库。
工具边界:纯人工,需要理解金库的份额定价逻辑。
中等

用的是 Solidity 0.8+ 还是 0.7 及更早?

工具能提示,判断需人工
为什么问:0.8 起内置溢出检查。0.7 及更早没有,必须依赖 SafeMath——漏一处就是一个溢出漏洞。
看哪里:看 pragma 声明。低于 0.8 就要逐个检查所有算术运算是否有 SafeMath 包裹。
工具边界:能提示存在 assembly(unchecked 块里常用),但判不了整体算术安全性。pragma 版本需要人看源码首行。(原理见代理存储冲突与未保护升级、拒绝服务(DoS))
致命

是否使用代理?实现合约地址是多少?升级权归谁?

工具可自动检查
为什么问:只看代理等于什么都没看——代理里通常只有 upgradeTo 几个函数,真实逻辑全在实现合约里。必须跟随到实现合约再分析。
看哪里:读 EIP-1967 槽、或 ZeppelinOS 槽(老式 USDC/USDT 用这个)。本站合约体检会自动识别并跟随。
工具边界:工具会自动读四种代理模式的槽位(含不做减一的 ZeppelinOS 变体)并跟随一层到实现合约,比人工快且不易漏。(原理见代理存储冲突与未保护升级)
致命

升级前后的存储变量顺序一致吗?有没有新变量插在中间?

只能人工检查
为什么问:在代理模式里插入变量会让后面所有变量错位,旧数据被当成新变量解释,直接损坏状态。
看哪里:对比两个版本的变量声明顺序,确认新变量只追加在末尾,且用了 storage gap(如 uint256[50] __gap)。
工具边界:需要两个版本的源码做差分,静态单文件扫描做不到。
致命

initialize 函数能否被重复调用?有没有 initializer 修饰器?

只能人工检查
为什么问:代理模式的构造函数不生效,初始化靠 initialize。若它能被再次调用,攻击者可以直接夺取所有权。
看哪里:搜 initialize,确认带 initializer 修饰器(OpenZeppelin Initializable),且实现合约的构造函数里调用了 _disableInitializers()。
工具边界:现有规则不检查初始化保护。这项是代理合约最高频的致命失误,必须人工确认。
致命

流动性锁定了吗?锁多久?锁定凭证的合约地址是什么?

只能人工检查
为什么问:没锁的流动性意味着项目方随时能撤池跑路。需要看到链上的锁仓合约地址,截图和公告都不算。
看哪里:在区块浏览器查 LP 代币的持有者,确认是否有已知的锁仓合约持有多数份额。
工具边界:完全不涉及源码分析,是链上事实核查。本站的合约体检能给持仓分布,但需要 API Key,因此默认降级不给出。
高危

持仓是否集中在少数地址?前 10 名占比多少?

只能人工检查
为什么问:如果前几名地址合计持有 80%,他们随时可以砸盘。需要区分「锁仓合约」「团队钱包」「交易所地址」——同样是高占比,含义完全不同。
看哪里:看区块浏览器的持币排行,逐个确认头部地址的性质。
工具边界:需要额外的链上索引或 API Key。本站对此明确降级为「不提供」而不是给一个猜测值。
致命

有没有「一笔交易内可完成的获利路径」?

只能人工检查
为什么问:闪电贷让攻击者无需本金就能临时动用巨额资金。如果协议的关键判断可以被一笔交易内的临时状态影响,那它就可以被零成本攻击。
看哪里:问自己:如果有人临时持有巨量代币 / 临时改变某个池子的储备,哪些判断会出错?
工具边界:这是清单里最重要的一条,也是工具最无能为力的一条。它要求的是对抗性思维,不是模式匹配。建议专门写一份「一笔交易内能做什么」的推演文档。
高危

审计报告原件能查到吗?审计的是哪个 commit?

只能人工检查
为什么问:「已审计」是营销词。要点:报告要能下载、要写明覆盖了哪些文件、要对应到具体的代码版本。审计完又改代码,审计就失效了。
看哪里:到审计公司官网或 GitHub 找原始报告,核对报告里的 commit hash 与当前部署的代码是否一致。
工具边界:完全的信息核查工作,与代码分析无关。
高危

审计范围覆盖了哪些合约?有没有「托管在范围外」的关键逻辑?

只能人工检查
为什么问:常见手法:只审计一个简单的代币合约,把真正有问题的经济逻辑放在另一个未审计的合约里。
看哪里:看报告的 scope 章节列出的文件路径,与链上实际部署的合约逐一对照。
工具边界:人工核对。
中等

有没有漏洞赏金计划?金额与项目 TVL 匹配吗?

只能人工检查
为什么问:赏金是「让攻击者算不过账」的经济手段。如果 TVL 一亿美元而最高赏金只有一万,理性的攻击者会选择直接攻击。
看哪里:在 Immunefi 等项目上查是否有该协议的赏金条目及最高奖励。
工具边界:信息来源在链下,工具无法覆盖。

这份清单来自真实审计流程里反复出问题的点,但它不是一份合规文件: 勾完只说明「这些该问的问题都问过了」,不构成对合约安全的判断。 想理解某一类漏洞为什么危险、怎么写才对,去漏洞库看可运行的两版代码对照。

为什么每项都要标「工具覆盖度」

清单最容易变成摆设:几十个勾选框全勾完、产出一份漂亮报告、然后什么都没查出来。 问题的根源在于——用户不知道自己刚才勾的那一下,到底验证了什么。

所以每一类都显式区分:有规则能给出证据行号、有规则但只能提示存在性、纯逻辑问题工具完全查不出。 第三类不是凑数的——历史上造成最大损失的几类漏洞(闪电贷操纵价格、签名重放、舍入套利) 全在第三类里,它们的代码在语法上完全正常。

完整覆盖 6 项

扫描引擎有对应规则,命中时直接给出文件与行号。这类项扫一遍就能拿到证据。

只能提示存在性

工具能看出「代码里出现了一个可改税率的 setter」, 但判不了它被设成 2 天时间锁还是被 Owner 随时一键改。

工具查不出 21 项

危险来自「这段代码在什么环境下运行」,不是写法。必须看懂业务的人逐条推演。

四步走完一遍

  1. 1先把Rug Pull 扫描器跑一遍,拿到有证据行号的命中列表——这些项可以直接标成「发现问题」,不用人工确认。
  2. 2剩下「只能提示存在性」的项,回到源码逐处确认参数与权限——是时间锁还是即时生效,差别就在这里。
  3. 3最后过「工具查不出」的 21 项。这一批花的时间最长, 但历史损失最大的问题也都在这里。每条都可以点「备注」把当时的判断记下来。
  4. 4导出 Markdown 报告。报告会显式列出「工具覆盖不到、且仍是未检查状态」的条目—— 这一节的存在是为了防止拿一份完整度虚高的报告去说服别人。

勾选状态保存在你这台机器的 localStorage 里,不会上传到任何地方。 换浏览器或清空站点数据会丢,所以要留档请导出报告。

10 个分组各查什么

权限与所有权

合约里最危险的东西不是 bug,而是「某个人能做什么」。先把特权函数和 Owner 是谁搞清楚,再谈其他。

代币发行与供应

增发权是归零最直接的方式:不需要攻击,持有者被稀释就够了。

转账限制与蜜罐

这一类决定了「你能不能卖出去」。静态扫描能提示存在性,但永远替代不了实际卖出测试。

外部调用与重入

只要合约会调用外部地址,就要问:调用之后我改状态了吗?对方的回调能重新进来吗?

价格来源与预言机

这一组全部是 none——历史损失最大的漏洞就在这里,而它们代码语法完全正常,静态扫描一条都抓不到。

签名与重放

签名本身不含「在哪条链、给哪个合约、第几次用」,这三件事必须显式写进去。

数学与舍入

整数除法会截断。在有份额、有价格的地方,截断的方向决定了谁能套利。

升级与存储

可升级合约的风险不在逻辑里,而在「版本之间」:存储槽冲突、初始化函数被重复调用。

经济与流动性

合约代码没问题,不代表项目没问题。这一组看的是代码之外的东西:流动性、持仓、代币分配。

审计与流程

"已审计"三个字必须能查到原始报告。这一组帮你判断一份审计报告到底覆盖了什么。

相关:想先弄懂某一项背后的原理,去常见漏洞库找对应的条目(清单里每一项都能一键跳到那里); 要查某个已知地址的验证状态与持仓分布,用合约体检;只有字节码想确认链上跑的是不是同一份,去字节码比对。