
Foundry reentrancy-eth 规则详解检测无 Gas 上限的 ETH 转账重入漏洞【免费下载链接】foundryFoundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.项目地址: https://gitcode.com/GitHub_Trending/fo/foundryreentrancy-eth是 Foundry 内置 Solidity 静态检查器forge lint中的一条 High 级别漏洞规则。它专门检测低层级.call{value: ...}转账未设置具体 gas 上限且调用前读取的合约状态变量在调用后才被写入这类经典的重入攻击模式。读完本文你将掌握该规则的检测逻辑、误报排除策略、命令行与配置文件用法并能结合 Foundry 源码与测试用例理解其实现原理在开发提款类withdraw合约时写出安全的代码。规则概述属性值规则 IDreentrancy-eth严重级别High检测目标未设置具体 gas 上限的 ETH 转账低层级调用包括gas: gasleft()后调用前已读取的状态变量被写入规则的完整英文说明位于 crates/lint/docs/reentrancy-eth.md源码实现位于 crates/lint/src/sol/high/reentrancy.rs配套测试用例为 crates/lint/testdata/ReentrancyEth.sol 与 crates/lint/testdata/ReentrancyEth.stderr。为什么无上限的.call{value: ...}是危险的与transfer、send不同低层级call默认会将当前函数剩余的全部 gas转发给被调方。这意味着恶意收款方可以在其fallback或receive函数中执行任意复杂逻辑并在调用方后续状态变更之前重新进入re-enter调用合约。如果函数在调用前读取了某个状态变量而对该变量的更新发生在调用之后那么重入方看到的就是一份过期stale的状态。它可能借此重复提款、打乱账本顺序造成资金损失。典型例子function withdraw() external { uint256 amount balances[msg.sender]; (bool ok, ) payable(msg.sender).call{value: amount}(); require(ok, transfer failed); balances[msg.sender] 0; }这里balances[msg.sender]在转账前被读出却在转账完成后才清零。攻击者在call触发其 fallback 时再次调用withdraw()仍能读到未清零的余额从而反复提取同一笔资金。规则检测了什么该规则报告同时满足以下条件的模式低层级调用形如addr.call{value: ...}(...)的调用实际转账value为非零值零值调用会被排除无具体 gas 上限未指定gas:或虽然指定了但值等于gasleft()——因为gasleft()约等于转发全部剩余 gas不构成真正的上限状态变量先读后写调用之前读取的某个状态变量在调用之后被写入。在源码层面判断无上限且带非零 value 的 call由 is_uncapped_value_call 完成它要求被调方是Member且成员名为callvalue选项非零且gas选项要么缺失、要么是gasleft()内置函数调用。规则明确排除的情形为避免误报reentrancy-eth对以下场景不发出告警仅事件顺序问题调用后只触发事件如emit Withdrawn(...)不写状态无关状态写入调用后写入的是与调用前读取的变量无关的其他状态变量零值调用value: 0或value: 编译期常量为 0 的常量构造函数中的调用构造函数执行期间合约尚未暴露外部入口无法被重入带具体 gas 上限的调用如gas: 5_000。需要特别强调的是文档明确指出仅有 gas 上限本身并不是重入防护。gas 上限的作用仅限于限制被调方在单次重入中可消耗的 gas例如低于transfer/send的 2300 gas stipend 时被调方无法执行复杂逻辑它不能替代先更新状态再交互checks-effects-interactions的正确顺序。正确写法先改状态后转账将该函数改写为先清零余额再进行外部转账即可同时消除重入窗口function withdraw() external { uint256 amount balances[msg.sender]; balances[msg.sender] 0; (bool ok, ) payable(msg.sender).call{value: amount}(); require(ok, transfer failed); }这也是文档推荐的修复方式状态变更effects先于外部交互interactions执行即使被调方重入读到的也是更新后的余额无法重复提款。源码实现路径敏感的流敏感分析reentrancy-eth是 crates/lint/src/sol/high/reentrancy.rs 中三个重入相关规则之一同文件还实现了reentrancy-balanceHigh检测先缓存address(this).balance再与过期余额比较与reentrancy-no-ethMed检测非 ETH 外部调用后的先读后写三个规则共用同一个Analyzer分析器。入口点识别分析器只处理外部可调用的非 view/pure 函数public/external 函数以及fallback、receive。判断逻辑位于 is_entry_point排除view/pure函数、排除构造函数并限定为 special 函数或Public/External可见性的普通函数。流状态FlowState跟踪分析器维护一个路径敏感的FlowState见 reentrancy.rs核心字段包括state_reads当前执行路径上已读取的状态变量集合pending_calls在状态读取之后发生的重入调用以及每个调用发生时尚未被后续写入的状态变量集合此外还有针对余额分析reentrancy-balance的各类本地变量追踪。每当遇到一次可重入调用push_call会把当前所有已读状态变量登记为等待后续写入见 reentrancy.rs而当分析器遇到对某个状态变量的写入时record_write会检查pending_calls中是否有调用正等待该变量被更新——若命中则在该调用处发出告警见 emit_pending_calls。分支与递归处理分支if/else、三元表达式、try/catch通过克隆状态、分别分析、最后合并的方式处理join_branches内部函数调用会被内联分析并通过inline_cache缓存结果递归调用通过recursive_cuts寻找递归截断点避免无限分析见 ReentrancyEthRecursiveStackRepro 对应的递归栈用例。测试用例解读规则的行为边界测试文件 crates/lint/testdata/ReentrancyEth.sol 以//compile-flags: --only-lint reentrancy-eth只启用本规则并在预期告警处用//~WARN:标注。从源码和 ReentrancyEth.stderr 中可以提炼出规则的关键行为边界场景是否告警说明读取balances后call{value: amount}再写入balances✅典型提款漏洞见withdraw调用后仅emit Withdrawn(...)❌仅事件顺序问题见withdrawWithEvent调用后写入无关变量totalPaid❌先读的是balances见unrelatedStateWriteAfterCallvalue: 0/value: ZERO常量 0❌零值调用被排除gas: gasleft()✅gasleft()不是真正的 gas 上限gas: 5_000具体数值❌有具体 gas 上限receiver.transfer(1 ether)❌transfer自带 2300 gas stipend调用发生在内部函数、修饰符modifier中✅跨函数/跨修饰符传播如sendValue、recordAfter复合赋值totalPaid[receiver] 1 ether先于调用✅见compoundAssignmentReadBeforeCall构造函数中的先读后写❌构造函数被排除值得注意的是即使函数带nonReentrant修饰符只要状态写入发生在调用之后规则依然告警见guardedByNonReentrant与ReentrancyEthNameOnlyGuard用例。这是因为该规则聚焦的是先读后写顺序这一结构问题而不是验证锁的实现是否正确——这也与文档gas 上限本身不是重入防护的立场一致。此外ReentrancyEth.sol还展示了跨内部函数追踪callInInternalHelper中调用被读状态在sendValue内部完成转账、回到外层再写入balances规则仍能跨函数边界准确告警。如何运行该规则在项目根目录执行forge lint --only-lint reentrancy-eth--only-lint按规则 ID 指定要运行的 lint并覆盖exclude_lints项目配置见 crates/forge/src/cmd/lint.rs 与 crates/forge/src/cmd/lint.rs 的解析逻辑。也可以按严重级别筛选全部 High 规则forge lint --severity High若只检查单个文件forge lint src/MyContract.sol配置文件方式在foundry.toml的[lint]段中可以配置 lint 行为例如排除某条规则[lint] exclude_lints [reentrancy-eth] severity [High]规则的启用与严重级别High定义在 reentrancy.rs 的declare_forge_lint!宏中并通过 crates/lint/src/sol/high/mod.rs 注册为late阶段 lint。关于 lint 的整体配置项with_severity、with_lints、without_lints等可参考 crates/lint/README.md。需要注意的是根据 crates/lint/README.md配置的测试与脚本目录下的文件默认不会被 lintunsafe-cheatcode等少数规则除外生产源码始终会被检查。总结与最佳实践reentrancy-eth用一条精确的规则覆盖了以太坊合约中最常见的高危漏洞模式之一在无 gas 上限的 ETH 转账之后才更新调用前读取的状态。它的排除清单零值调用、事件顺序、无关写入、构造函数、具体 gas 上限有效控制了误报率而其路径敏感、跨内部函数与修饰符的流分析则保证了检测深度。日常开发中建议提款类函数严格遵循 checks-effects-interactions 顺序先更新状态再做外部转账即使使用nonReentrant锁或 gas 上限也不要忽视先读后写的顺序问题——这些手段都不能替代正确的状态更新顺序将forge lint --severity High纳入 CI 流程让reentrancy-eth、reentrancy-balance等 High 级规则在合并前拦截风险。【免费下载链接】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),仅供参考