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——历史损失最大的漏洞就在这里,而它们代码语法完全正常,静态扫描一条都抓不到。

签名与重放

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

数学与舍入

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

升级与存储

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

经济与流动性

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

审计与流程

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

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