
Foundry forge lint 规则详解delegatecall-loop 与可支付循环中的 delegatecall 风险【免费下载链接】foundryFoundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.项目地址: https://gitcode.com/GitHub_Trending/fo/foundry本文基于 Foundry 仓库中forge lint的delegatecall-loop规则文档位于 crates/lint/docs/delegatecall-loop.md系统讲解该规则检测的代码模式、背后的安全动机、官方推荐的修复写法并结合仓库内的 Rust 实现与测试用例delegatecall_loop.rs、DelegatecallLoop.sol还原其检测原理。读完本文你将掌握在什么场景下delegatecall与循环的组合是危险的、如何写出既安全又能通过 lint 检查的批量分发代码以及如何用forge lint在项目中启用或排除这条规则。规则速览项目内容规则 IDdelegatecall-loop严重级别Low低所属类别潜在安全风险Payable 函数相关检测对象for、while、do while循环体中出现的delegatecall表达式附加条件包裹该循环的外层入口函数必须是public payable或external payable诊断消息payable function usesdelegatecallinside a loop在 Foundry 的 linter 分类中见 crates/lint/README.md该规则与calls-loop、block-timestamp等同属 Low 严重级别且属于默认启用范围默认severity为 High、Med、Low 三档见下文配置说明。规则检测什么payable 循环体内的 delegatecall规则文档对检测范围的表述非常精确Reportsdelegatecallexpressions that appear in the body of afor,while, ordo whileloop when the enclosing function ispublic payableorexternal payable.即同时满足以下两个条件的代码会被报告delegatecall出现在for、while或do while的循环体内包括for的更新表达式等每次迭代都会执行的位置该循环最终位于一个public payable或external payable入口函数的执行路径上。需要注意的是条件 2 中的“入口函数”判定并不仅限于字面意义上的“同一函数体内”。从实现看详见下文源码解析规则会沿着函数调用链与 modifier 链追踪只要循环最终在 payable 入口函数内执行无论是写在 helper 内部函数中、modifier 中还是通过super派发到达都会被覆盖。官方给出的触发示例function batch(address[] calldata receivers) external payable { for (uint256 i; i receivers.length; i) { address(this).delegatecall(abi.encodeWithSignature(credit(address), receivers[i])); } }这段代码把一次msg.value在循环中重复传递给多个delegatecall正是规则要拦截的典型模式。为什么这是危险的规则文档给出的安全动机如下delegatecall会在调用方的存储上下文中执行另一份合约的代码并原样保留调用时的msg.sender与msg.value在一个 payable 函数中如果循环体内多次执行delegatecall即使 Ether 只被转入一次循环中的每一次委托调用看到的都是同一个msg.value如果被委托的代码逻辑依赖msg.value记账那么一笔交易就可能把同一笔付款重复入账多次或者以非预期的方式反复改写调用方的存储状态。简言之风险本质是「一次入账、多次消费」msg.value是调用级语义而非单次调用语义循环会放大这种语义错配造成重复记账或状态被反复变更。值得补充的是delegatecall自身的信任模型被委托代码可任意读写调用方存储在 Foundry 的另一条规则controlled-delegatecallHigh 级别见 crates/lint/README.md中单独处理delegatecall-loop聚焦的是「循环 payable delegatecall」这一组合模式。官方推荐的修复写法规则文档给出了修复建议的核心思路在进入循环前一次性完成金额分配计算循环内改用普通内部调用而不是在循环体内反复delegatecall。官方示例等额均分场景function batch(address[] calldata receivers) external payable { require(receivers.length ! 0, no receivers); require(msg.value % receivers.length 0, unequal split); uint256 share msg.value / receivers.length; for (uint256 i; i receivers.length; i) { _credit(receivers[i], share); } }文档特别提醒了几个边界细节这些是实际落地时必须考虑的点拒绝空列表require(receivers.length ! 0)避免除零msg.value / 0会回滚但显式检查语义更清晰也避免无意义的空转整除校验require(msg.value % receivers.length 0)确保每份金额严格相等不会因取整产生余数余数处理策略该示例只接受恰好整除的支付其他 API 设计可以显式退款或把余数单独记账需要在业务层明确选择一种策略并实现循环内调用_credit这类内部函数内部调用运行在同一个执行上下文中传递的是显式计算的share参数msg.value不再被隐式重复消费。源码级实现这条规则是如何检测的规则的 Rust 实现位于 crates/lint/src/sol/low/delegatecall_loop.rs核心逻辑非常精简implgcx LateLintPassgcx for DelegatecallLoop { fn check_function(mut self, ctx: LintContext, gcx: Gcxgcx, func: gcx Functiongcx) { for_each_payable_loop_expr(gcx, func, |expr| { if let ExprKind::Call(callee, ..) expr.kind gcx.resolved_builtin(callee) Some(Builtin::AddressDelegatecall) { ctx.emit(DELEGATECALL_LOOP, expr.span); } }); } }它通过declare_forge_lint!宏声明了规则元数据ID 为delegatecall-loop、Severity 为Low并实现LateLintPass的check_function回调。该规则在 crates/lint/src/sol/low/mod.rs 中以late阶段注册。检测逻辑可分为两层第一层遍历「payable 函数循环内的所有表达式」for_each_payable_loop_expr定义在共享工具模块 crates/lint/src/sol/low/payable_loop.rs 中该模块同时服务所有*-loop系列 lint。入口过滤条件如下if !matches!(func.kind, FunctionKind::Constructor | FunctionKind::Modifier) func.state_mutability StateMutability::Payable matches!(func.visibility, Visibility::Public | Visibility::External)即被检查的函数必须是payable且为public / external的普通函数构造函数与 modifier 本身不作为入口判定。从源码结构看LoopWalker通过维护loop_depth计数来识别循环上下文并做三件关键的事情展开 modifier 链沿 modifier 的_占位符StmtKind::Placeholder递归进入被包裹的函数体因此「modifier 内含循环 函数体含 delegatecall」或「modifier 循环包裹函数体的 delegatecall」都能被追踪对应测试中的payableModifierLoop与payableModifierLoopPlaceholder用例内联内部 helper 调用对可解析的内部函数调用直接调用、库/基类限定调用、using for绑定、super派发递归展开其函数体因此「payable 函数在循环内调用内部函数、而 delegatecall 藏在内部函数里」也能被捕获正确解析super与虚函数分派LoopWalker记录dispatch入口函数所属合约与current当前代码所在合约通过合约线性化linearization解析super.next(...)最终落到哪个实现避免多级继承下漏报或误报。同时callee()方法只内联is_internal()或is_delegate_call()的函数对「合约类型值上的外部调用」包括this上的调用一律不展开从而与外部delegatecall区分开。第二层识别真正的 delegatecall 内建调用回到delegatecall_loop.rs对于遍历到的每个循环内表达式只有满足表达式是调用ExprKind::Call且调用目标解析为 Solidity 内建函数address.delegatecallBuiltin::AddressDelegatecall通过gcx.resolved_builtin(callee)判定才会触发诊断。这意味着自定义接口里恰好名为delegatecall的普通函数不会被误报——测试用例OrdinaryDelegatecall接口与payableLoopCallsOrdinaryDelegatecall函数验证了这一点。同样call/staticcall、非 payable 函数中的循环 delegatecall都不会命中该规则。测试用例覆盖矩阵与阴性对照规则配套的测试文件 crates/lint/testdata/DelegatecallLoop.sol 以//compile-flags: --only-lint delegatecall-loop声明只运行该规则并在每个期望命中的位置以//~WARN: payable function usesdelegatecallinside a loop标注预期输出对应 DelegatecallLoop.stderr。这份用例本身也是一份很好的「触发条件速查表」应当命中WARN的场景场景说明payableForLoop/payableWhileLoop/payableDoWhileLoop三种循环形态全覆盖payableNestedDelegatecalldelegatecall 嵌套在循环内的if分支中payableForUpdateExpressiondelegatecall 出现在for的更新表达式里每次迭代都会执行payableModifierLoopmodifier 内含循环函数体使用该 modifierpayableModifierLoopPlaceholdermodifier 用_循环包裹函数体函数体内有 delegatecallpayableLoopWithInternalDelegatecall循环调用内部函数delegatecall 藏在内部函数内helper 内联payableInternalLoopWithDelegatecall内部函数自带循环 delegatecall被 payable 入口调用payableLoopWithSuperInternalDelegatecall/payableLoopWithLinearizedSuperDelegatecallsuper派发与多级继承线性化场景DelegatecallLoopLib.helper库内 helper库函数中的循环 delegatecall 被入口函数循环调用payableLoopCallsConditionalDelegatecall(WithBinaryCondition)三元表达式选择 delegatecall 目标payableLoopCallsThisDelegatecall/payableLoopCallsSuperDelegatecallthis.与super.形式的 delegatecall 调用不应命中阴性对照的场景nonPayableLoop非 payable 函数中循环内 delegatecall——不报payableLoopWithCallAndStaticcall循环内call/staticcall——不报仅delegatecall内建触发payableLoopCallsOrdinaryDelegatecall调用接口中自定义的、非内建的delegatecall函数——不报payableDelegatecallOutsideLooppayable 函数中但不在循环内的 delegatecall——不报payableLoopCallsSafeOverload与overloaded重载解析循环调用被重载的内部函数需确认解析到不含 delegatecall 的重载——不报。这些用例同时验证了检测器的两个关键工程细节跨函数/跨 modifier 的追踪必须依赖可靠的调用图与继承解析而识别必须严格限定在内建delegatecall语义上二者缺一不可。在项目中使用与配置该规则delegatecall-loop作为默认启用的 Low 级别规则开箱即用。相关配置入口如下1.forge lint命令行命令实现见 crates/forge/src/cmd/lint.rs# 全项目默认检查Low 在默认 severity 范围内会自动包含本规则 forge lint # 只看某条规则--only-lint 会绕过 severity 过滤只运行指定 ID forge lint --only-lint delegatecall-loop # 按严重级别筛选 forge lint --severity low要点--only-lint指定规则 ID 列表并覆盖exclude_lints配置--severity覆盖项目配置中的severity合法值包括high、med、low、info、gas见 crates/config/src/lint.rs 中的Severity枚举与解析。2.foundry.toml项目配置对应配置结构定义在 crates/config/src/lint.rs 的LinterConfig[lint] # 运行哪些严重级别的规则默认 high/med/low本规则属于 low severity [high, med, low] # 排除指定 ID 的规则 exclude_lints [delegatecall-loop] # 忽略匹配 glob 的文件 ignore [test/**, script/**] # 是否在 forge build 时自动执行 lint默认 true lint_on_build true另外根据 crates/lint/README.md 的说明test与script目录下的文件默认不参与除unsafe-cheatcode和environment-read-across-mutation之外的所有规则包括显式指定时生产源码始终会被检查此例外仍受 severity 过滤、排除项与行内抑制inline suppression约束。3. 诊断输出示例命中时输出形如取自 DelegatecallLoop.stderrwarning[delegatecall-loop]: payable function uses delegatecall inside a loop ╭▸ src/YourContract.sol:LL:CC │ LL │ (bool ok,) target.delegatecall(payloads[i]); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ ╰ help: forge lint 规则文档 delegatecall-loop如需在 CI 或脚本中以机器可读格式消费诊断可配合 JSON 输出forge lint还提供--report-unused-suppressions用于检查未被实际使用的行内抑制注释。与其他相关规则的边界calls-loopLow循环中的外部调用可能因单次 revert 或 gas 耗尽导致批量操作整体 DoS。它与delegatecall-loop的关注点不同——前者是「循环内任意外部调用」的可用性问题后者是「payable delegatecall」的资金语义问题controlled-delegatecallHighdelegatecall的目标必须是可证明受信任的地址关注「调谁」delegatecall-loop关注「在什么上下文里调」payable 循环low-level-callsInfo/Style主张尽量避免直接使用底层调用属于风格层面的通用建议。三者可以叠加使用controlled-delegatecall保证目标可信、delegatecall-loop阻止 payable 循环中滥用、low-level-calls引导整体减少底层调用面。小结一条可执行的检查清单结合规则文档、源码实现与测试矩阵接入该规则时的实践要点可归纳为识别高危模式public/external payable函数中循环体内出现address(...).delegatecall(...)无论直接书写还是藏在内部函数、modifier、super派发、库函数中理解风险本质delegatecall保留msg.sender与msg.value循环会让「一次转账、多次消费」成为可能造成重复记账或存储被反复改写优先重构而非抑制进入循环前完成均分计算显式拒绝空列表与不整除情形、明确余数处理策略循环内改用传递显式参数的内部函数调用善用工具链forge lint --only-lint delegatecall-loop单独验证foundry.toml的[lint]段统一管理 severity 与排除项测试目录天然豁免生产源码始终在检查范围内保持规则边界认知delegatecall-loop只针对「payable 循环 内建 delegatecall」这一特定组合call/staticcall、非 payable 函数、自定义同名接口函数均不属于其职责范围。【免费下载链接】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),仅供参考