Foundry Forge 静态分析:reentrancy-balance 规则如何识别过期合约余额重入 📅 发布时间:2026/9/17 11:43:28 👁 浏览次数: Foundry Forge 静态分析reentrancy-balance 规则如何识别过期合约余额重入【免费下载链接】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中的reentrancy-balance规则ID 为reentrancy-balance严重级别 High。该规则专门检测一类隐蔽的重入漏洞函数先保存address(this).balance随后发起允许重入的外部调用再用过期余额与当前余额比较来证明支付完成。读完本文你将理解该规则的完整触发条件、底层数据流分析原理、nonReentrant守卫的抑制机制以及如何在项目中启用与规避误报。规则概览检测什么、报告什么依据 crates/lint/docs/reentrancy-balance.md 的定义该规则报告同时满足以下三个条件的 public/external 函数保存了address(this).balance合约自身 ETH 余额到某个局部变量之后发起了允许重入的外部调用再之后把当前余额与该已保存的旧值做比较。规则只关心合约自身的 ETH 余额不涉及 ERC-20 等代币余额也不涉及其他地址的余额。其规则元数据声明在 reentrancy.rsdeclare_forge_lint!( REENTRANCY_BALANCE, Severity::High, reentrancy-balance, external call can be reentered before a stale contract balance is checked );修复建议来自原文档function mint(uint256 amount) external payable nonReentrant { require(msg.value amount, insufficient payment); _mint(msg.sender, amount); }即用调用时随交易附带的msg.value替代调用前余额快照 调用后余额复核这种易被重入击穿的支付校验模式并辅以nonReentrant锁。为什么危险过期的余额基线而非比较运算符原文档给出了漏洞成因的精准表述回调可以在任何一次调用到达余额检查之前多次重入目标函数。于是嵌套的多次重入调用共享同一个调用前余额一次真实支付就能同时满足多次操作的校验。关键洞察是非严格不等式、并不能防御这种攻击。因为攻击的根源不在于使用了哪个比较运算符而在于被比较的基线baseline已经过期——所有嵌套调用读到的是同一次调用前的旧值重入者不需要任何净余额变化即可通过检查。这一点在Why is this bad一节中被明确强调也是该规则与一般检查-生效-交互CEI类规则的本质区别。触发模式三步判定与源码级实现从实现看该规则由Analyzer以数据流分析方式执行核心状态记录在 FlowState 中包括balance_local_paths哪些局部变量派生自address(this).balancebalance_local_forms/balance_local_dependencies这些值上承载的算术形态与依赖关系pending_balance_calls在缓存了余额局部变量之后发生的外部调用即使余额过期的调用balance_comparison_locals与过期余额进行比较的局部变量invalidated_balance_guards被写坏或绕过而失效的重入锁。emit_balance_calls在守卫条件处检查若pending_balance_calls中某个调用满足比较的一方依赖调用前余额、另一方依赖调用后余额则在该外部调用位置报告警告见 emit_balance_calls。比较判定覆盖Lt/Le/Gt/Ge/Eq/Ne全部六种运算符见 guard_has_stale_balance_comparison因此无论使用哪种比较运算符只要基线与当前余额跨越了同一外部调用都会命中——这正呼应了非严格不等式无效的结论。范围限定入口点与调用类型入口点限定仅分析 public/external 函数、fallback与receive且排除 view/pure 函数与构造函数见 is_entry_point。排除 view/static 调用view、staticcall无法修改状态回调中不能重入写路径因此不视为可重入调用。2300 gas 上限豁免transfer/send转发的气体津贴是 2300若被调用方总计最多只能获得 2300 gas含转账附带的 stipend则不具备重入能力规则将其排除。该阈值定义在 REENTRANCY_GAS_STIPEND判定逻辑见第 1748 行附近的gas REENTRANCY_GAS_STIPEND || (sends_eth !gas.is_zero())。典型漏洞示例原文档示例function mint(IPayer payer, uint256 amount) external { uint256 balanceBefore address(this).balance; payer.pay(); require(address(this).balance balanceBefore amount, insufficient payment); _mint(msg.sender, amount); }payer.pay()是攻击者可控的外部调用攻击者可以在该回调内反复重入mint每次重入都看到同一个balanceBefore只需支付一次即可铸造多份代币。守卫抑制机制什么算有效防护原文档指出一个标准的nonReentrant锁只要覆盖了每一个可变入口点就可以抑制该警告。这是有明确语义依据的——既然所有可变入口都被锁住回调中无法再次进入受保护的写路径余额过期就失去了利用条件。测试用例 ReentrancyBalance.sol 对守卫的判定边界刻画得非常细致直接nonReentrant修饰器ReentrancyBalanceGuardedL809-L827无警告修饰器委托给内部函数ReentrancyBalanceHelperGuardedL829-L859无警告只有函数加了锁、其他入口点未加锁ReentrancyBalanceCrossFunctionGuardL921-L948加锁函数无警告未加锁的同类函数仍警告守卫在调用前被手动解锁ReentrancyBalanceInvalidatedGuard.unlocksBeforeCallL895-L904警告重新出现delegatecall可能改写锁状态存储ReentrancyBalanceDelegateInvalidatesGuardL950-L971警告锁在嵌套代码块内被提前释放ReentrancyBalanceNestedPlaceholderUnlockL1016-L1037警告空壳nonReentrant只改名不改行为ReentrancyBalanceNameOnlyGuardL906-L919警告——规则识别的是实际的锁变量读写语义而非修饰器名字。测试矩阵识别能力的边界ReentrancyBalance.sol约 1000 行系统性地验证了规则在各种代码形态下的判定对应的期望输出见 ReentrancyBalance.stderr。值得关注的代表性场景场景代表用例是否警告require复核余额requireAfterCallL41-L48是revert分支复核revertingBranchAfterCallL50-L56是基线先做加法再比较derivedBaselineL58-L63是this地址别名局部变量、helper 返回值、元组赋值selfAddressAlias、helperReturnedSelfAddressAlias等是检查发生在外部调用之前checkBeforeInteractionL376-L380否基线在调用后被重新赋值overwrittenBaselineL382-L387否比较对象被重定向到其他地址reassignedSelfAddressAliasL119-L128否检查其他地址的余额otherAddressBalanceL672-L680否检查代币余额tokenBalanceL682-L692否view调用/staticcallviewCallCannotReenter、viewSameNamedMethod否显式gas: 2_300封顶concreteGasCapL709-L714否封顶但携带 ETH 转账{value: 1, gas: 2_300}、{value: amount, gas: 2_300}是{value: 0, gas: 2_300}/{value: 1, gas: 0}valueStipendGasCapL253-L263否通过内部 helper、函数指针、递归间接调用callThroughHelper、callThroughFunctionPointer、recursiveHelper是循环内的余额检查loopCarriedCheck、continueGuard、breakGuard是条件路径互补路径谓词分析sequentialComplementaryPaths、sequentialSamePath依路径判定比较结果经 helper 返回后使用comparisonReturnedByHelper是非比较形式的余额运算无法证明是过期比较nonComparisonReturnedByHelper否值得注意的是最后一行规则只报告被证明是过期余额比较的情形对于无法归约为current vs stale baseline的纯算术表达式如current balanceBefore amount分析器选择保守不报告避免误报。在 Foundry 中启用与运行reentrancy-balance属于High 严重级别规则默认随forge lint一并启用完整规则清单见 crates/lint/README.md。可以仅运行该规则以聚焦检查forge lint --only-lint reentrancy-balance--only-lint参数在测试夹具中同样使用——ReentrancyBalance.sol 文件头部的编译指令//compile-flags: --only-lint reentrancy-balance表明测试框架正是以此方式单独选中该规则。两条使用前提需要留意test 与 script 目录默认豁免根据 crates/lint/README.md 的说明除unsafe-cheatcode和environment-read-across-mutation外其他规则包括本规则在配置的 test/script 目录下不会触发即使显式选中也是如此——生产源码始终会被检查。因此测试合约中的同类写法不会产生警告需要你自行审查。警报定位警告被报告在可重入外部调用所在的行见 ReentrancyBalance.stderr 中callback.pay()处的高亮帮助快速定位使余额过期的那个调用点而不是余额检查行。小结reentrancy-balance精准刻画了保存余额快照 → 可重入外部调用 → 复核余额这一漏洞三角它识别的是过期基线问题因此无论比较使用何种运算符都难逃检测它以数据流分析追踪余额的派生、别名与算术形态同时通过 2300 gas 豁免、view 调用排除和nonReentrant守卫识别控制误报。结合 reentrancy.rs 的实现与 ReentrancyBalance.sol 的测试矩阵开发者既能理解规则行为也能准确预判自己代码的检查结果从而在开发早期消除此类隐蔽重入漏洞。【免费下载链接】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),仅供参考