在以太坊智能合约开发中,数学运算的安全性至关重要,因为错误的运算可能导致溢出、截断或其他漏洞,从而危及合约的安全性和可靠性。Solidity 是一种静态类型语言,早期版本(0.8.0 之前)对整数溢出没有内置保护,因此开发者需要特别注意。
数学运算中的安全问题
常见风险
Solidity 中的数学运算可能面临以下风险:
- 整数溢出/下溢 :在 0.8.0 之前的版本中,
uint256
类型加法可能从最大值溢出到 0,减法可能从 0 下溢到最大值。 - 截断 :在不同位宽类型(如
uint256
到uint128
)转换时,数据可能丢失。 - 除零:除法运算未检查除数为 0,可能导致运行时错误。
- 精度丢失:处理浮点数或小数运算时(如在 DeFi 中),可能因整数运算导致精度问题。
Solidity 0.8.0+ 的改进
从 Solidity 0.8.0 开始,默认启用溢出检查:
+
,-
,*
,/
,%
等运算符会检查溢出/下溢,溢出时抛出Panic(0x11)
错误。- 无需显式检查,简化开发,但仍需注意其他问题(如除零、精度)。
尽管如此,开发者仍需掌握安全数学运算的最佳实践,以应对复杂场景和兼容旧版本。
实现安全的数学运算
使用 Solidity 0.8.0+ 的内置溢出检查
在 0.8.0 及以上版本中,Solidity 自动为 uint
和 int
类型添加溢出检查。
示例:简单的加法
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract SafeMathBasic {
function add(uint256 a, uint256 b) public pure returns (uint256) {
return a + b; // 自动检查溢出
}
function subtract(uint256 a, uint256 b) public pure returns (uint256) {
return a - b; // 自动检查下溢
}
}
说明:
a + b
如果溢出,会抛出Panic(0x11)
。a - b
如果下溢,同样抛出错误。- 无需额外代码,适合简单场景。
测试用例:
javascript
const { expect } = require("chai");
describe("SafeMathBasic", function () {
let SafeMathBasic, contract;
beforeEach(async function () {
SafeMathBasic = await ethers.getContractFactory("SafeMathBasic");
contract = await SafeMathBasic.deploy();
await contract.deployed();
});
it("should add correctly", async function () {
expect(await contract.add(100, 200)).to.equal(300);
});
it("should revert on overflow", async function () {
await expect(contract.add(ethers.MaxUint256, 1)).to.be.revertedWithPanic("0x11");
});
it("should subtract correctly", async function () {
expect(await contract.subtract(200, 100)).to.equal(100);
});
it("should revert on underflow", async function () {
await expect(contract.subtract(100, 200)).to.be.revertedWithPanic("0x11");
});
});
运行测试:
bash
npx hardhat test
注意:
- 内置溢出检查增加少量 Gas 成本,但安全性更高。
- 对于高性能场景,可使用
unchecked
块禁用检查(见下文)。
使用 unchecked
块(谨慎使用)
在 Solidity 0.8.0+ 中,unchecked
块可禁用溢出检查,用于优化 Gas 或特定场景(如模运算)。
示例:使用 unchecked
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract UncheckedMath {
function addUnchecked(uint256 a, uint256 b) public pure returns (uint256) {
unchecked {
return a + b; // 溢出时环绕
}
}
}
说明:
unchecked
允许溢出环绕(如uint256.max + 1 = 0
)。- 仅在明确需要环绕行为(如哈希计算)时使用。
- 风险:可能导致意外行为,需谨慎测试。
测试用例:
javascript
const { expect } = require("chai");
describe("UncheckedMath", function () {
let UncheckedMath, contract;
beforeEach(async function () {
UncheckedMath = await ethers.getContractFactory("UncheckedMath");
contract = await UncheckedMath.deploy();
await contract.deployed();
});
it("should wrap around on overflow", async function () {
expect(await contract.addUnchecked(ethers.MaxUint256, 1)).to.equal(0);
});
});
注意:
- 仅在明确了解溢出后果时使用
unchecked
。 - 添加显式检查以确保结果符合预期。
使用 OpenZeppelin 的 SafeMath 库(0.8.0 之前)
对于 Solidity < 0.8.0,推荐使用 OpenZeppelin 的 SafeMath
库,显式检查溢出。
示例:使用 SafeMath
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.6;
import "@openzeppelin/contracts/math/SafeMath.sol";
contract SafeMathLegacy {
using SafeMath for uint256;
function add(uint256 a, uint256 b) public pure returns (uint256) {
return a.add(b); // 检查溢出
}
function subtract(uint256 a, uint256 b) public pure returns (uint256) {
return a.sub(b); // 检查下溢
}
function multiply(uint256 a, uint256 b) public pure returns (uint256) {
return a.mul(b); // 检查溢出
}
function divide(uint256 a, uint256 b) public pure returns (uint256) {
return a.div(b); // 检查除零
}
}
说明:
SafeMath
提供add
,sub
,mul
,div
等函数,自动检查溢出和除零。- 使用
using SafeMath for uint256
简化调用。 - 适合 0.8.0 之前版本,0.8.0+ 可省略。
测试用例:
javascript
const { expect } = require("chai");
describe("SafeMathLegacy", function () {
let SafeMathLegacy, contract;
beforeEach(async function () {
SafeMathLegacy = await ethers.getContractFactory("SafeMathLegacy");
contract = await SafeMathLegacy.deploy();
await contract.deployed();
});
it("should add correctly", async function () {
expect(await contract.add(100, 200)).to.equal(300);
});
it("should revert on overflow", async function () {
await expect(contract.add(ethers.constants.MaxUint256, 1)).to.be.revertedWith("SafeMath: addition overflow");
});
it("should divide correctly", async function () {
expect(await contract.divide(100, 4)).to.equal(25);
});
it("should revert on division by zero", async function () {
await expect(contract.divide(100, 0)).to.be.revertedWith("SafeMath: division by zero");
});
});
安装 OpenZeppelin:
bash
npm install @openzeppelin/contracts@3.4.2
注意:
- 0.8.0+ 不推荐使用
SafeMath
,因为内置溢出检查已足够。 - 对于旧版本,
SafeMath
是行业标准。
处理除零和精度问题
除零检查: 即使在 0.8.0+,除法仍需显式检查除数不为 0。
示例:
solidity
function divide(uint256 a, uint256 b) public pure returns (uint256) {
require(b != 0, "Division by zero");
return a / b;
}
精度问题: Solidity 不支持浮点数,需使用整数模拟小数运算(如 DeFi 的利率计算)。
示例:处理小数
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract SafeDecimalMath {
uint256 public constant DECIMAL_PRECISION = 1e18; // 18 位小数
function multiplyDecimal(uint256 a, uint256 b) public pure returns (uint256) {
return (a * b) / DECIMAL_PRECISION;
}
function divideDecimal(uint256 a, uint256 b) public pure returns (uint256) {
require(b != 0, "Division by zero");
return (a * DECIMAL_PRECISION) / b;
}
}
说明:
- 使用
DECIMAL_PRECISION
(如 1e18)模拟小数运算。 - 先乘后除(如
(a * DECIMAL_PRECISION) / b
)避免精度丢失。 - 常用于 DeFi 协议(如 Uniswap)。
测试用例:
javascript
const { expect } = require("chai");
describe("SafeDecimalMath", function () {
let SafeDecimalMath, contract;
beforeEach(async function () {
SafeDecimalMath = await ethers.getContractFactory("SafeDecimalMath");
contract = await SafeDecimalMath.deploy();
await contract.deployed();
});
it("should multiply decimals correctly", async function () {
const a = ethers.parseUnits("2", 18); // 2.0
const b = ethers.parseUnits("3", 18); // 3.0
expect(await contract.multiplyDecimal(a, b)).to.equal(ethers.parseUnits("6", 18));
});
it("should divide decimals correctly", async function () {
const a = ethers.parseUnits("6", 18); // 6.0
const b = ethers.parseUnits("2", 18); // 2.0
expect(await contract.divideDecimal(a, b)).to.equal(ethers.parseUnits("3", 18));
});
it("should revert on division by zero", async function () {
await expect(contract.divideDecimal(100, 0)).to.be.revertedWith("Division by zero");
});
});
自定义安全数学库
对于特殊需求,可实现自定义安全数学函数。
示例:自定义加法和乘法
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract CustomSafeMath {
error Overflow();
error Underflow();
error DivisionByZero();
function safeAdd(uint256 a, uint256 b) public pure returns (uint256) {
uint256 c = a + b;
if (c < a) revert Overflow();
return c;
}
function safeMul(uint256 a, uint256 b) public pure returns (uint256) {
if (a == 0 || b == 0) return 0;
uint256 c = a * b;
if (c / a != b) revert Overflow();
return c;
}
function safeDiv(uint256 a, uint256 b) public pure returns (uint256) {
if (b == 0) revert DivisionByZero();
return a / b;
}
}
说明:
- 使用自定义错误(
Overflow
,Underflow
,DivisionByZero
)节省 Gas。 safeMul
检查c / a == b
确保无溢出。- 适合 0.8.0 之前或需要特殊检查的场景。
测试用例:
javascript
const { expect } = require("chai");
describe("CustomSafeMath", function () {
let CustomSafeMath, contract;
beforeEach(async function () {
CustomSafeMath = await ethers.getContractFactory("CustomSafeMath");
contract = await CustomSafeMath.deploy();
await contract.deployed();
});
it("should add correctly", async function () {
expect(await contract.safeAdd(100, 200)).to.equal(300);
});
it("should revert on overflow", async function () {
await expect(contract.safeAdd(ethers.MaxUint256, 1)).to.be.revertedWithCustomError(contract, "Overflow");
});
it("should multiply correctly", async function () {
expect(await contract.safeMul(10, 20)).to.equal(200);
});
it("should revert on division by zero", async function () {
await expect(contract.safeDiv(100, 0)).to.be.revertedWithCustomError(contract, "DivisionByZero");
});
});
综合案例:安全的 DeFi 存款合约
以下是一个安全的 DeFi 存款合约,结合内置溢出检查和自定义小数运算。
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract SafeDeposit is ReentrancyGuard {
uint256 public constant DECIMAL_PRECISION = 1e18;
mapping(address => uint256) public balances;
uint256 public totalDeposits;
event Deposited(address indexed user, uint256 amount, uint256 scaledAmount);
event Withdrawn(address indexed user, uint256 amount, uint256 scaledAmount);
error InsufficientBalance();
error InvalidAmount();
function deposit(uint256 amount, uint256 interestRate) public payable nonReentrant {
if (amount == 0 || msg.value != amount) revert InvalidAmount();
// 计算利息(示例:固定利率)
uint256 scaledAmount = (amount * interestRate) / DECIMAL_PRECISION;
balances[msg.sender] += scaledAmount;
totalDeposits += scaledAmount; // 自动检查溢出
emit Deposited(msg.sender, amount, scaledAmount);
}
function withdraw(uint256 amount) public nonReentrant {
if (amount > balances[msg.sender]) revert InsufficientBalance();
balances[msg.sender] -= amount;
totalDeposits -= amount; // 自动检查下溢
payable(msg.sender).transfer(amount);
emit Withdrawn(msg.sender, amount, amount);
}
}
说明:
- 使用 0.8.0+ 的内置溢出检查。
DECIMAL_PRECISION
处理小数运算。ReentrancyGuard
防止重入攻击。- 事件记录存款和取款操作。
测试用例:
javascript
const { expect } = require("chai");
describe("SafeDeposit", function () {
let SafeDeposit, contract, owner, user;
beforeEach(async function () {
SafeDeposit = await ethers.getContractFactory("SafeDeposit");
[owner, user] = await ethers.getSigners();
contract = await SafeDeposit.deploy();
await contract.deployed();
});
it("should deposit with interest", async function () {
const amount = ethers.parseEther("1");
const interestRate = ethers.parseUnits("1.1", 18); // 1.1x
await expect(contract.connect(user).deposit(amount, interestRate, { value: amount }))
.to.emit(contract, "Deposited")
.withArgs(user.address, amount, ethers.parseUnits("1.1", 18));
expect(await contract.balances(user.address)).to.equal(ethers.parseUnits("1.1", 18));
expect(await contract.totalDeposits()).to.equal(ethers.parseUnits("1.1", 18));
});
it("should revert on overflow", async function () {
await contract.connect(user).deposit(ethers.parseEther("1"), ethers.parseUnits("1", 18), { value: ethers.parseEther("1") });
await expect(contract.connect(user).deposit(ethers.MaxUint256, ethers.parseUnits("2", 18), { value: ethers.MaxUint256 }))
.to.be.revertedWithPanic("0x11");
});
it("should withdraw correctly", async function () {
const amount = ethers.parseEther("1");
await contract.connect(user).deposit(amount, ethers.parseUnits("1", 18), { value: amount });
await expect(contract.connect(user).withdraw(amount))
.to.emit(contract, "Withdrawn")
.withArgs(user.address, amount, amount);
expect(await contract.balances(user.address)).to.equal(0);
});
it("should revert on insufficient balance", async function () {
await expect(contract.connect(user).withdraw(ethers.parseEther("1")))
.to.be.revertedWithCustomError(contract, "InsufficientBalance");
});
});
运行测试:
bash
npx hardhat test