Foundry 规则深度解析:divide-before-multiply 先除后乘精度丢失检测
【免费下载链接】foundryFoundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.项目地址: https://gitcode.com/GitHub_Trending/fo/foundry
divide-before-multiply是 Foundry 内置 Solidity 静态检查器(forge lint)中的一条**中危(Med)**规则,用于拦截(a / b) * c这类"先整数除法、再乘法"的表达式。由于 Solidity 的整数除法向零截断,这种写法会在放大之前就丢弃余数,是 DeFi 合约中手续费、份额、收益计算精度偏差的常见来源。读完本文,你将理解该规则的触发原理、修复写法与溢出权衡,并通过仓库源码与测试用例掌握其检测边界,能在foundry.toml中正确启用并落地到自己的合约工程中。
规则概览:它检测什么
该规则的完整定义为(见 crates/lint/src/sol/med/div_mul.rs):
| 属性 | 值 |
|---|---|
| 规则 ID | divide-before-multiply |
| 严重级别 | Med(中危) |
| 触发形态 | (a / b) * c及其等价写法 |
| 警告信息 | division before multiplication may lose precision |
从源码实现看,declare_forge_lint!宏将该规则注册为LateLintPass(在语义分析阶段之后运行),入口为check_function——即对每个函数的函数体进行逐语句、逐表达式的检查,而不是简单的正则或文本匹配。这意味着它理解 Solidity 的语法与类型,能识别括号、复合表达式、赋值乃至内联汇编中的真实除法/乘法语义。
为什么这是坏的:整数除法的精度陷阱
Solidity 的整数除法向零截断。对表达式(a / b) * c,求值顺序是:先执行a / b得到截断后的商,丢弃余数,再乘以c放大;而(a * c) / b先在整数域内完成缩放再做除法,保留了更多精度。
以文档中的示例为例:
// 先除后乘:截断发生在缩放之前 uint256 share = (amount / total) * weight; // 先乘后除:保留精度 uint256 share = (amount * weight) / total;取amount = 100、total = 3、weight = 2:
(100 / 3) * 2 = 33 * 2 = 66——100 / 3先截断为 33,损失了1/3;(100 * 2) / 3 = 200 / 3 = 66——仍会截断,但截断发生在最终一步,中间没有二次精度损失。
两种写法在数值上可能恰好相等,也可能相差巨大,取决于操作数关系。仓库测试用例 DivideBeforeMultiply.sol 中覆盖了多种形态,例如(1 / 2) * 3、3 * (1 / 2)、4 * ((1 + 2) / 3),以及分母中出现"先乘后除"的1 / ((2 / 3) * 3)也会被警告——因为被乘数/除数本身就是除法结果时同样存在精度损失。
在实际业务中,该模式常体现为手续费计提、代币份额分配、收益率换算等场景的系统性小数偏差,日积月累可被利用或导致账目失衡。
修复写法与溢出权衡
文档明确指出:先乘后除并不总是安全的。(a * c) / b中的a * c可能溢出,即使最终结果恰好适配当前整数类型。因此选择重写时需要明确两点:
- 确认乘积不溢出:当
a * c的中间乘积能安全放入该整数类型(如uint256)时,直接改写为先乘后除; - 否则使用全精度辅助函数:改用检查式乘法/除法(checked 乘法,或专门的"全精度乘法-除法"库函数),如 OpenZeppelin 风格的
mulDiv实现; - 显式决定舍入行为:计算需要向零舍入、向上舍入还是四舍五入,应根据业务语义显式选择,而不是依赖求值顺序偶然决定的截断。
也就是说,规则的意图不是机械地要求"一律先乘后除",而是提醒开发者对除法后的乘法保持警惕,并显式决策精度与舍入策略。
从源码看检测能力:污点数据流分析
该规则的实现远比"匹配(a / b) * c文本"强大。核心是一个污点分析(taint analysis):以Tainted = HashSet<VariableId>记录"当前值来自除法结果"的局部变量集合,然后沿控制流传播(见 div_mul.rs)。
污点的设置与传播
- 变量声明:
uint256 q = a / b;后q被标记为污点;q = a + b;清除污点(div_mul.rs)。 - 普通赋值:右值如果是除法或污点变量,左值继承污点;元组解构赋值(
(x, y) = (a / b, c))会逐元素判定,而不是整体污染(rhs_taints函数,见 div_mul.rs)。 - 复合赋值:
q *= c若q带污点则直接告警;q /= b会重新设置污点;而q += 1、q++等会清除污点(见 div_mul.rs)。测试函数compoundClearsTaint、incrementClearsTaint验证了这一点。 - 别名与复制:
uint256 copy = q; return c * copy;污点会随变量复制继续传播。
控制流感知
污点并非简单地"沾上就不掉",而是按分支、循环做保守合并:
- if 分支:任一可达分支落下时,合并各分支的污点(
check_branches,见 div_mul.rs)。测试中branchPropagated(条件分支内赋值后使用)会告警。 - return / revert 分支不泄漏:若某分支以
return或revert终止,则该分支内的污点不会污染后续代码——对应测试returningBranchDoesNotLeak、revertingBranchDoesNotLeak均不告警。 - 循环:循环体内产生的污点会传播到循环后使用(
loopPropagated告警)。 - try/switch:同样按互斥分支合并污点。
- 函数调用参数:调用并不会把普通参数"宽泛地"视为除法结果(
noBroadRhsTaint测试验证了helper(a / b)不会污染helper(q)之后的乘法)。
这种设计使规则既能发现跨语句、跨分支的精度问题,又避免了对无关代码的误报。
内联汇编(Yul)检测
通过is_yul_call识别 Yul 内建函数,规则同样覆盖assembly块(见 div_mul.rs):
assembly { result := mul(div(a, b), c) // 告警:div 后 mul result := mul(c, sdiv(a, b)) // 告警:sdiv(有符号除法)后 mul let q := div(a, b) result := mul(q, c) // 告警:污点变量参与 mul }有符号除法sdiv与无符号div都会被跟踪;测试文件通过--only-lint divide-before-multiply custom-errors inline-assembly编译标志同时开启了inline-assembly规则以验证这一路径(见 DivideBeforeMultiply.sol)。
如何启用与使用
forge lint会在forge build的编译后阶段自动运行(lint_on_build,见 crates/forge/src/cmd/build.rs)。针对本项目,可执行:
# 仅运行本规则(配合其他规则过滤,参考测试用例的编译标志) forge lint --only-lint divide-before-multiply # 跳过编译后的 lint 步骤(相当于 foundry.toml 中 [lint] lint_on_build = false) forge build --no-lint在foundry.toml中可通过[lint]段控制(对应 build.rs 中severity、exclude_lints、lint_specific等配置的解析):
[lint] lint_on_build = true severity = ["warn"] # 至少以警告级别展示 exclude_lints = [] # 例如 ["divide-before-multiply"] 可排除本规则注意:当前 lint 引擎(Solar)仅支持 Solidity >= 0.8.0的源码(见 build.rs),低于该版本的合约不会被此规则分析。
参考与深入阅读
- 规则文档原文:crates/lint/docs/divide-before-multiply.md
- 规则实现源码:crates/lint/src/sol/med/div_mul.rs
- 规则注册(
med级别模块):crates/lint/src/sol/med/mod.rs - 测试用例(正反例全集):crates/lint/testdata/DivideBeforeMultiply.sol
- 测试期望输出(stderr 快照):crates/lint/testdata/DivideBeforeMultiply.stderr
- 规则清单与说明:crates/lint/README.md
- lint 引擎接入点(build 阶段):crates/forge/src/cmd/build.rs
【免费下载链接】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),仅供参考