资讯动态

Foundry `forge lint` 零地址检查规则 `missing-zero-check` 深度解析:从检测原理到“假守卫“谓词识别修复

发布时间:2026/9/16 21:34:24 来源:尧图企业网站定制
Foundryforge lint零地址检查规则missing-zero-check深度解析从检测原理到假守卫谓词识别修复【免费下载链接】foundryFoundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.项目地址: https://gitcode.com/GitHub_Trending/fo/foundry导读本文围绕 Foundry 内建 Solidity 静态检查器forge lint的 Low 级别规则missing-zero-check展开完整讲解该规则的检测范围、触发条件、修复写法并深入 规则实现源码 剖析其污点分析原理以及一次针对引用了地址参数却未证明其非零的谓词被误当作守卫的关键修复见 变更记录。读完本文你将理解missing-zero-check的判定边界什么样的谓词才算有效的零地址守卫什么样的形似守卫会被识破并继续告警。规则概览项目内容规则 IDmissing-zero-check严重级别Low默认启用forge lint默认运行 High / Med / Low 三档规则诊断消息address parameter is used in a state write or value transfer without a zero-address check官方文档crates/lint/docs/missing-zero-check.md实现源码crates/lint/src/sol/low/missing_zero_check.rs该规则在 crates/lint/README.md 中被归入 Low Severity 类别与calls-loop、deprecated-oz-function、reentrancy-events等规则并列。规则检测什么根据官方规则文档missing-zero-check会在外部可调用的状态变更函数或构造函数中报告那些被用于状态写入或价值转移、却没有针对address(0)进行检查的address类型参数。从实现源码看missing_zero_check.rs判定入口点entry point需要同时满足函数状态可变性不是pure或view且满足以下之一是构造函数func.is_constructor()是普通函数且可见性为public或external。也就是说internal/private的辅助函数不在检测范围内view/pure函数即使引用了地址参数也不会触发如测试中的viewer、pureFn、noSinkJustPassthrough均通过。测试用例可见 crates/lint/testdata/MissingZeroCheck.sol。该规则识别的风险操作sink主要有两类状态写入将地址写入一个address类型的状态变量例如owner newOwner价值/调用转移作为transfer、send、call、delegatecall的接收者源码通过address_call_receiver识别见 crates/lint/src/sol/analysis/exprs.rs。staticcall接收者不算因为不涉及状态写入或价值转移。为什么这是一个值得关注的问题忘记零地址检查是价值损失的常见来源代币被永久锁死在零地址无法找回所有权被意外放弃合约陷入无人管理状态升级逻辑被永久卡死如 proxy 的实现地址被置零。显式添加一个守卫成本极低却能消除一整类操作性失误。示例错误写法与正确写法错误写法规则文档原始示例function setOwner(address newOwner) external onlyOwner { owner newOwner; // no zero-address check }正确写法function setOwner(address newOwner) external onlyOwner { require(newOwner ! address(0), zero address); owner newOwner; }除require外assert、if (...) revert();、自定义error等显式退出方式同样被认可为守卫详见下文哪些谓词算数。源码级原理基于污点追踪的分析器MissingZeroCheck是一个LateLintPass延迟 lint pass在 HIR 语义分析完成后运行核心由一个Analyzer结构体承载missing_zero_check.rs维护三张关键集合taint从候选地址参数传递导出的变量集合映射到其源头参数。初始时每个参数映射到自身sinks已经到达风险操作状态写入 / 转移的源头参数guarded截至目前已被证明非零的源头参数。分析流程可以概括为污染传播 守卫记录 汇点命中三步1. 候选参数收集只收集address类型含address payable的函数参数L48-L52。没有地址参数则直接返回。2. 修饰器modifier预处理函数上的修饰器如果直接以标识符形式传入地址参数则把修饰器体当作函数前缀进行分析L55-L74。例如modifier nonZero(address a) { require(a ! address(0), zero); _; } function setOwner(address newOwner) external nonZero(newOwner) { owner newOwner; }nonZero(newOwner)中实参是直接标识符映射成立修饰器内的守卫会作用于newOwner因此该函数通过检查。反之如果实参是表达式如nonZero(addrIdentity(a))映射无法建立守卫不被认可仍然会告警见测试modifierWithExpr。3. 守卫识别nonzero_facts这是本次修复的核心所在。nonzero_facts从谓词中提取非零事实L126-L155其规则是形如x ! 0或取反后的x 0的比较且另一侧必须是零值常量is_zero_value才能证明x非零对!取反递归处理对/||复合条件按布尔逻辑合并两侧事实A B在肯定分支上取并集A || B只有两侧同时成立才能确认……源码中(And, false) | (Or, true)取并集其余取交集对应短路求值语义。is_zero_valuecrates/lint/src/sol/analysis/exprs.rs只认可数字字面量0地址字面量address(0)零地址布尔false以及包裹零值的单参数 cast。关键点函数调用如lookup(0)、状态变量如owner、address(this)、mapping 查找如whitelisted[a]、~uint160(0)这类位运算表达式都不是零值常量。因此形如require(a ! address(this))、require(a ! owner)、require(whitelisted[a])的谓词虽然引用了地址参数却没有证明参数非零——这正是本次补丁.changelog/missing-zero-check-content.md标注forge: patch修复的内容Fixedmissing-zero-checkaccepting predicates that referenced an address parameter without proving it non-zero.修复前这类引用了参数但并未证明其非零的谓词可能被错误地当作有效守卫导致漏报修复后它们不再被认可参数依旧会被标记告警。4. 汇点命中与守卫作用域在visit_expr中当表达式是地址状态变量的赋值、或transfer/send/call/delegatecall的接收者时进入汇点深度sink_depth此时命中的标识符如果其污点源src不在guarded集合中就被记入sinksL275-L281。守卫的控制流作用域也经过精心处理if 语句then / else 分支各自的守卫通过scoped_guards隔离若某分支必然退出branch_always_exits见 crates/lint/src/sol/analysis/stmts.rs覆盖return、revert、退出型调用、assert(false)等则该分支守卫可延续到 if 之后否则要求两个分支都守卫才成立L189-L212循环体循环可能执行零次循环内守卫不延续到循环外guardInForLoop、guardInWhileLoop因此告警try/catch各子句只走一条路径子句内守卫同样被丢弃guardInTryClause告警顺序性守卫必须出现在 sink之前guardAfterSink告警。哪些谓词算数PASS 与 FAIL 边界测试用例对照完整用例见 crates/lint/testdata/MissingZeroCheck.sol 与期望输出 crates/lint/testdata/MissingZeroCheck.stderr。会被告警FAIL /~WARN标记的典型场景场景原因owner newOwner无任何检查直接缺省构造函数constructor(address initialOwner)构造函数同样需要检查to.transfer(1)/to.send(1)/to.call(data)/a.delegatecall()价值/调用转移 sink无实际作用的修饰器doesNothing(a)修饰器内无守卫别名传播address tmp a; owner tmp;污点经局部变量传播重新赋值tmp a;同上类型转换owner address(uint160(a));cast 不丢失源头三目表达式flag ? a : address(0)取别名后仍无法保证非零payable(a).transfer(1)包裹转换不影响修饰器实参是表达式nonZero(addrIdentity(a))无法证明守卫适用于参数同一参数喂两个 sink只产生一条诊断去重多跳传播x a; y x; owner y;多级污染传播守卫在 sink 之后顺序无效守卫只在单分支/只在循环内/只在 try 子句内作用域不覆盖require(a ! address(this))谓词引用参数但未证明非零本次修复重点require(whitelisted[a])同上mapping 查找不是零值比较require(a ! owner)同上状态变量不是零值常量require(amt 0 to ! msg.sender)未与零值比较require(a ! lookup(0))lookup是函数调用调用结果不是零值常量require(a ! address(~uint160(0)))位运算不是零值字面量require(a address(0))要求等于零恰好相反require(a ! address(0) || flag)析取一侧可假不能保证if (a address(0)) owner a;sink 在为零分支内必然使用零地址通过检查PASS的场景require(newOwner ! address(0), zero); // 标准 require assert(newOwner ! address(0)); // assert if (newOwner address(0)) revert(); // if revert if (a address(0)) revert ZeroAddress(); // 自定义 error function f(address newOwner) external nonZero(newOwner) // 修饰器直接传参此外还包括双分支对称守卫guardOnBothBranches、分支内嵌套守卫配合提前revertnestedGuardWithRevert、then 分支必然退出require(false)/assert(false)/return而 else 分支守卫setOwnerRequireFalseInThen等、取反合取require(!(a address(0) || b address(0)))、无关合取 真实检查require(amt 0 newOwner ! address(0))、以及if (a ! address(0)) owner a;sink 在非零分支内天然安全。如何运行与配置在项目根目录执行forge lint只运行本规则测试文件也使用该方式见 MissingZeroCheck.sol 的//compile-flags: --only-lint missing-zero-checkforge lint --only-lint missing-zero-check--only-lint会绕过严重级别过滤实现见 crates/forge/src/cmd/lint.rs。此外forge geiger本质上就是forge lint --only-lint unsafe-cheatcode的别名crates/forge/src/cmd/geiger.rs。foundry.toml中可配置[lint]段配置结构见 crates/config/src/lint.rs[lint] # 运行哪些严重级别默认 [high, medium, low] severity [high, medium, low] # 排除指定规则 ID exclude_lints [missing-zero-check] # 忽略的 glob ignore [test/**] # 是否在 forge build 时自动执行 lint默认 true lint_on_build true注意missing-zero-check属于Low级别只要默认的severity配置包含low默认即如此就会被运行若自定义severity时漏掉low该规则会被跳过。局限性只针对函数参数含修饰器直接传参不追踪状态变量本身、外部调用返回值等来源守卫只认可与零值常量的直接比较address(this)、其他状态变量、函数调用结果等均不算数——这是刻意的保守设计宁可多报也不漏报对internal/private辅助函数、view/pure函数不做检测规则为启发式静态分析无法覆盖动态数据流如 storage 读入后再写入的场景。小结missing-zero-check是forge lint中低成本、高回报的防御性规则通过污点传播 零值守卫识别 汇点命中的三段式分析捕捉地址参数未经address(0)校验即进入状态写入或价值转移的路径。而本次补丁进一步收紧了守卫的判定口径——只有与零值常量的比较才能证明非零杜绝了谓词引用了参数、却并未证明其非零造成的漏报使规则在require(a ! address(this))、require(whitelisted[a])这类写法面前依然保持告警帮助开发者把零地址风险真正扼杀在代码审查阶段。【免费下载链接】foundryFoundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.项目地址: https://gitcode.com/GitHub_Trending/fo/foundry创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价