第 5 章 | Solidity 合约中的整数溢出与精度陷阱全解析
🧮 第 5 章 | Solidity 合约中的整数溢出与精度陷阱全解析
——YAM 崩盘与算术误差复盘,如何正确使用 SafeMath 与 unchecked?
✅ 本章导读
在 Solidity 合约开发中,最危险的错误往往不是逻辑问题,而是你以为算对了,但其实错了。
合约里的资产转账、比例分配、利息计算、奖励发放,全都涉及到整数运算与精度控制。
一旦数值不准,不是项目经济模型失衡,就是直接被套利、爆仓。
本章我们将系统拆解 Solidity 合约中常见的数值问题:
-
整数溢出与下溢(0.8.x 以后真的安全吗?)
-
运算顺序导致的精度丢失(浮点错误在 Solidity 是常态)
-
decimals 精度管理误差(ERC20 中最常见的坑)
-
unchecked{}
的使用与滥用 -
真正安全的乘除方式写法模板
-
真实事故复盘(YAM、Balancer)
1️⃣ 什么是整数溢出 / 下溢(Overflow / Underflow)?
✅ 概念
-
Solidity 使用固定大小的整数类型,如
uint256
(0 ~ 2²⁵⁶-1) -
溢出(Overflow):变量加法超过最大值 → 回绕成 0
-
下溢(Underflow):减法减出负数(uint 无法表示) → 回绕成最大值
✅ 例子(Solidity 0.7.x 以下)
uint8 a = 255;
a += 1; // ❌ 溢出 → a = 0
uint8 b = 0;
b -= 1; // ❌ 下溢 → b = 255
✅ Solidity 0.8.x 后自动加入检查
// Solidity 0.8+ 中这段代码将 revert:
uint256 a = 2**256 - 1;
a += 1; // 报错:overflow
✅ 但为什么 0.8.x 依然可能出错?
因为很多开发者在 unchecked{}
中包裹运算:
function uncheckedAdd(uint a, uint b) public pure returns (uint) {
unchecked {
return a + b;
}
}
⚠️ 如果不明原因使用 unchecked
,你等于手动关闭了安全带。
在性能需求极高的情况下确实可用,但必须经过严格测试。
2️⃣ 精度陷阱:为什么 1e18 * 0.25 有时 = 0?
✅ Solidity 不支持浮点数
你必须使用整数来近似表示所有“浮点”数据。
例如:
uint reward = total * 0.25; // ❌ 错误,0.25 会变成 0
✅ 正确做法:
uint reward = total * 25 / 100;
或者用 18 位精度管理:
uint reward = total * 25e16 / 1e18; // 25e16 = 0.25 * 1e18
3️⃣ 乘除顺序陷阱:一换顺序,直接归零
❌ 错误示范:
uint result = 1000 * 1e18 / 1e20; // ➜ 结果为 0
解释:1000 * 1e18 = 1e21
,再除 1e20
,得到 10。
但如果你先 1e18 / 1e20 = 0
,整个式子就变成 1000 * 0 = 0
!
✅ 正确姿势:先乘后除
Never divide first. Always multiply first.
result = amount * ratio / scale;
4️⃣ ERC20 decimals 精度误差导致金额失真
✅ 案例 1:忘记考虑 token decimals
// 代币 A 有 6 decimals,但我们按 1e18 运算,结果发错数量
aToken.transfer(user, amount * reward / total); // ❌
解决方式:
uint tokenDecimals = 10 ** IERC20Metadata(token).decimals();
...
uint reward = total * userShare / poolTotal;
reward = reward / 1e18 * tokenDecimals;
5️⃣ 案例复盘:YAM 与 Balancer
💥 YAM 崩盘(2020)
-
问题: 在
rebase()
逻辑中执行复合计算 -
totalSupply = totalSupply * price / targetPrice
-
忘记更新精度乘数导致乘除顺序不当
-
结果出现整数溢出,DAO 无法投票提案修复
📎 结局:项目治理瘫痪,只能重启部署
💥 Balancer 被套利(2021)
-
问题: 奖励代币复利结算计算精度错误
-
攻击者通过 Flashloan 操控
ratePerSecond
,精准制造浮点误差 -
每轮套利可稳定提取超额奖励
📎 教训:任何时间、利息、折扣计算都要使用 FixedPointMathLib
等高精度工具
✅ 推荐写法模板:安全数值操作
// 推荐使用 Foundry 的 fixed point math lib
import "solmate/utils/FixedPointMathLib.sol";
// 假设 reward = baseAmount * rate / 1e18
uint reward = FixedPointMathLib.mulDivDown(baseAmount, rate, 1e18);
或使用:
SafeMath.mul().div()
✅ 本章实战模板代码:安全乘除结构
function calculateReward(uint amount, uint rewardRate, uint base) public pure returns (uint) {
// reward = amount * rewardRate / base
// 避免精度丢失
return amount * rewardRate / base;
}
或:
function safeMulDiv(uint a, uint b, uint denom) internal pure returns (uint) {
return a * b / denom;
}
🛠 精度审计 Checklist(可直接用于项目自检)
检查项 | ✅/❌ |
---|---|
是否统一使用了 18 位精度(或 token decimals) | ✅ |
是否所有比例运算都使用“乘后除”结构? | ✅ |
是否使用 FixedPointMathLib / SafeMath? | ✅ |
是否所有乘除操作都在 0.8+ 安全检查下运行? | ✅ |
是否误用 unchecked{} 封装关键运算? | ✅ |
🧪 课后挑战
-
写一个
calcReward()
函数,接受 3 个参数:用户 stake、池子总量、奖励总额-
实现高精度奖励分配
-
加入 decimals 参数支持不同 token
-
模拟 10000 用户参与下的极小精度误差(unit test)
-
-
模拟 YAM 的溢出 bug
-
创建一个带
rebase()
函数的合约 -
故意写错乘除顺序,复现溢出
-
修复后对比 Gas 与正确性
-
✅ 本章总结
-
Solidity 没有浮点数,所有比例运算都要手动模拟
-
0.8+ 自动检查溢出已提高安全,但不是万能
-
unchecked{}
只能用于确定不会出错、且性能敏感的逻辑 -
精度管理 = 财务安全的第一步,不能凭经验猜
✅ 下一章预告|第 6 章:预言机操控与闪电贷攻击
👉 Mango Markets 如何操控喂价 + 抬高自身抵押品价值?
👉 如何在合约中正确使用 Chainlink 与 TWAP?
👉 闪电贷攻击的核心机制、常用攻击路径、真实复现案例