
Foundry Lint 规则实战incorrect-strict-equality——对余额使用严格相等比较的隐患检测【免费下载链接】foundryFoundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.项目地址: https://gitcode.com/GitHub_Trending/fo/foundry本文围绕 Foundryforge lint内置的 Medium 级规则incorrect-strict-equality展开讲解它如何识别对 ETH 余额与 ERC-20 余额使用/!的危险写法为什么这类比较会被selfdestruct强制转账与直接代币转账轻易破坏以及如何结合源码实现与测试用例理解其判定边界最终在项目里用 CLI 与foundry.toml正确启用并修复此类问题。读完本文你将能准确判断自己的代码是否会命中该规则并掌握/或内部记账等安全替代方案。规则速览项目内容规则名称incorrect-strict-equality严重级别Med中等触发消息dangerous strict equality check on an externally-influenced value检查时机Late 阶段语义分析后的 HIR 遍历源码位置crates/lint/src/sol/med/incorrect_strict_equality.rs注册位置crates/lint/src/sol/med/mod.rs该规则在 Foundry 的 lint 规则目录中被归入 Medium 级别官方描述为 Dangerous strict equality check on an externally-influenced value (ETH balance, ERC-20 balance)见 crates/lint/README.md。它检测什么两类被标记的表达式规则会报告任何或!严格比较表达式只要其左侧或右侧操作数中包含以下两类值之一expr.balance——某个地址的 ETH 余额例如address(this).balanceexpr.balanceOf(args)——一次 ERC-20 代币余额查询调用例如token.balanceOf(address(this))。值得注意的是检测并不局限于余额直接作为比较操作数的情况。只要操作数子树中任意位置出现余额读取就会被标记。例如// 余额参与算术后被比较——同样被标记 function ethBalanceInArith() public view returns (bool) { return address(this).balance 1 100 ether; } function tokenBalanceInArith() public view returns (bool) { return threshold token.balanceOf(address(this)) - 1; }从 crates/lint/testdata/IncorrectStrictEquality.sol 的用例可以看到无论是还是!、余额在左边还是右边、是否被 1/- 1等算术包裹都会触发//~WARN。不标记的例外msg.value精确校验文档明确说明精确的msg.value校验不会被该规则标记。例如要求固定付款金额function pay() external payable { require(msg.value 1 ether, wrong amount); }这是因为msg.value由调用者显式指定、属于付款即校验的正常模式与会被外部任意改动的账户余额性质不同。测试文件 IncorrectStrictEquality.sol 中对此有专门注释说明。为什么危险外部可控值上的精确相等这些被标记的值都可以被合约控制范围之外的第三方影响因此以它们为基准的精确相等判断是脆弱的ETH 余额攻击者可以通过selfdestruct向任意地址强制转账 ETH不经过任何函数、不触发任何逻辑。如果一个提款锁依赖address(this).balance target这样的精确判断一次强制捐赠就能让该条件永久不可达锁被绕过或直接锁死bricked。ERC-20 余额代币可以被直接转账到合约地址无需触发合约内的任何 hook。例如token.balanceOf(address(this)) 0这样的空合约守卫攻击者只需捐赠 1 wei 代币即可将其打破。文档给出的危险示例// ETH balance可被 selfdestruct 捐赠锁死 function withdraw() external { require(address(this).balance 100 ether, wrong balance); payable(owner).transfer(address(this).balance); } // ERC-20 balance1 wei 直接转账即可绕过 function claimWhenEmpty() external { require(token.balanceOf(address(this)) 0, not empty); // ... }推荐修复思路文档给出的替代方案有两类一是改用/表达不低于/不高于的不变式容忍外部注入的资金二是放弃读取链上余额改为内部记账在每次入账/出账时维护自己的uint256计数变量// 用 / 容忍外部额外注入的资金 function withdraw() external { require(address(this).balance 100 ether, insufficient balance); payable(owner).transfer(address(this).balance); } function claimWhenEmpty() external { // 改为内部记账不再依赖链上余额 require(internalBalance 0, not empty); }源码实现原理入口与注册规则通过declare_forge_lint!宏声明元信息并实现LateLintPasstraitincorrect_strict_equality.rsdeclare_forge_lint!( INCORRECT_STRICT_EQUALITY, Severity::Med, incorrect-strict-equality, dangerous strict equality check on an externally-influenced value );随后在 crates/lint/src/sol/med/mod.rs 中以late阶段注册incorrect_strict_equality: (IncorrectStrictEquality, late, (INCORRECT_STRICT_EQUALITY));check_expr只在二元比较节点上检查核心逻辑位于LateLintPass::check_exprincorrect_strict_equality.rs。只有当表达式是二元运算、且运算符为Eq或Ne!时才进入判定随后对lhs与rhs两侧分别做递归visit只要任意一侧子树中存在外部可影响值即触发报告。这解释了前面提到的算术包裹、嵌套在函数调用实参里也会被标记的现象——因为判定是递归遍历整棵操作数 AST 的。is_externally_influenced两种模式的精确识别is_externally_influencedincorrect_strict_equality.rs是规则的判别核心分两种情形fn is_externally_influencedgcx(gcx: Gcxgcx, expr: Exprgcx) - bool { match expr.peel_parens().kind { ExprKind::Member(..) gcx.resolved_builtin(expr) Some(Builtin::AddressBalance), ExprKind::Call(callee, ..) { matches!(callee.peel_parens().kind, ExprKind::Member(base, m) if m.as_str() balanceOf !matches!(referenced_item(gcx, base), Some(ItemId::Contract(cid)) if gcx.hir.contract(cid).kind.is_library())) } _ false, } }关键设计点.balance必须被解析为内置的AddressBalance借助 Solar 语义分析器的resolved_builtin判断接收者可证明是地址类型从而避免把结构体中恰好名为balance的uint256字段误报见下文的边界测试。balanceOf按方法名匹配它被认定为压倒性的 ERC-20 方法命名但会跳过对 library 的静态调用referenced_item解析出基对象是 library 时排除避免误伤库中同名的内部辅助函数。误报抑制的边界设计从源码注释与测试用例可以归纳出该规则的三个刻意豁免场景非地址类型上的balance字段——struct Account { uint256 balance; }中的account.balance 0不触发本地结构体变量的balance字段——Account memory a; a.balance 100不触发静态库调用名为balanceOf的内部函数——TokenUtils.balanceOf(token, address(this)) 0不触发。对应用例见 IncorrectStrictEquality.sol。触发边界一览结合测试用例仓库中的回归测试文件 crates/lint/testdata/IncorrectStrictEquality.sol 通过//compile-flags: --only-lint incorrect-strict-equality注解单独运行本规则完整覆盖了 24 个 SHOULD FAIL 与 10 个 SHOULD PASS 场景。会触发SHOULD FAIL的典型形态场景示例address(this).balance的/!address(this).balance 1 ether余额在比较右侧1 ether address(this).balance余额参与算术address(this).balance 1 100 ethertoken.balanceOf(...)的/!token.balanceOf(address(this)) 0出现在require条件中require(token.balanceOf(address(this)) 0, not empty)出现在if条件中if (address(this).balance 0)各种地址来源的.balancerecipient.balance、a.balance、payable(a).balance、msg.sender.balance、tx.origin.balance、block.coinbase.balance、account.owner.balance、holder.operator.balance、holders[i].balance、holderOf[i].balance、getRecipient().balance余额藏在三元表达式、函数调用实参中(flag ? address(this).balance : 0) 0、consume(address(this).balance) 0不会触发SHOULD PASS的场景场景示例使用//address(this).balance 1 ether、token.balanceOf(address(this)) threshold普通uint256变量比较x thresholdmsg.sender地址比较msg.sender account_精确msg.value校验msg.value 1 ether非地址类型上的balance字段account.balance 0、Account memory a; a.balance 100静态库调用balanceOfTokenUtils.balanceOf(token, address(this)) 0对应的诊断输出快照保存在 IncorrectStrictEquality.stderr共 24 条 warning格式为warning[incorrect-strict-equality]: dangerous strict equality check on an externally-influenced value并以下划线标注出问题的比较表达式、附带 help 链接提示。如何在项目中使用命令行运行forge lint命令定义在 crates/forge/src/cmd/lint.rs。最常用的三种方式# 只运行本规则 forge lint --only-lint incorrect-strict-equality # 按严重级别过滤本规则属于 Med forge lint --severity med # 指定文件/目录覆盖 ignore 配置 forge lint src/Counter.sol从 lint.rs 的源码可见当显式传入--only-lint时会绕过严重级别过滤器将 severity 置空只执行用户指定的 lint ID未传入时则使用命令行--severity或配置文件中的severity并叠加exclude_lints排除项。foundry.toml 配置LinterConfig定义在 crates/config/src/lint.rs支持以下配置项配置项默认值说明severity[high, med, low]要运行的 lint 级别支持high/med/low/info/gas/codesizeexclude_lints[]按 ID 排除指定 lint例如[incorrect-strict-equality]ignore[]需要忽略的 glob 路径lint_on_buildtrue是否在forge build时自动运行 lint示例[lint] severity [high, med, low] exclude_lints [] [lint.ignore] # 需要忽略的路径 glob由于默认severity包含med且lint_on_build默认为true在forge build阶段就会自动执行本规则构建路径见 crates/forge/src/cmd/build.rs其将exclude_lints注入 linter。另外按照 crates/lint/README.md 的说明项目配置的 test 与 script 目录默认不参与除unsafe-cheatcode、environment-read-across-mutation之外的 lint生产源码始终会被检查。最佳实践小结能用/表达不变式就不要用/!比较余额从而对selfdestruct强制转账与直接代币捐赠保持鲁棒性涉及是否为空/是否为特定金额的资产状态判断优先采用内部记账变量而非链上余额快照msg.value的精确校验是合法模式不会被本规则误报无需改写结构体字段、库函数等与本规则同名但不相关的写法已被源码级豁免可直接依赖规则结果而无需额外豁免配置在 CI 或提交前运行forge lint --only-lint incorrect-strict-equality作为针对性门禁或依赖forge build的默认自动检查。【免费下载链接】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),仅供参考