第三节:整数溢出漏洞——Solidity 0.8之前的溢出风险、SafeMath库的使用、NFT合约中的数量计算陷阱

整数溢出,说白了就是数字“撑爆了”。

你想想看,一个uint8能存的最大值是255。如果你给它加1,它会变成0,而不是256。这在Solidity 0.8之前,是默认行为——不会报错,不会回滚,直接给你一个错误的结果。

我个人习惯把这种漏洞叫做“沉默的炸弹”。因为它不会爆炸,但结果已经错了。我在审计NFT合约时,至少见过三次因为这个漏洞导致铸币数量失控的案例。

1. 为什么0.8之前这么危险?

Solidity 0.8之前,整数溢出是静默溢出。什么意思?

  • uint8 a = 255; a + 1 的结果是0
  • uint8 b = 0; b - 1 的结果是255
  • 没人告诉你出错了,合约继续执行

我记得有一次,一个项目方在mint函数里写了这样的逻辑:

// 危险代码!Solidity 0.8之前
function mint(address to, uint256 amount) public {
    require(amount > 0);
    _totalSupply += amount;  // 如果_totalSupply接近最大值,这里会溢出
    _balances[to] += amount; // 同理
}

嗯,这里要注意:_totalSupply是uint256,最大值是2^256-1。理论上很难溢出,但如果你允许用户自己指定amount,或者合约里有循环累加的逻辑,溢出是可能发生的。

⚠️ 核心风险点:
  • 铸币数量计算:totalSupply + mintAmount 可能溢出
  • 转账余额计算:balance + transferAmount 可能溢出
  • 批量操作:循环累加时,中间变量可能溢出

2. SafeMath库——曾经的救星

在0.8之前,OpenZeppelin的SafeMath库几乎是每个项目的标配。它的原理很简单:每次运算后检查结果是否正确,如果溢出就revert。

// SafeMath库的核心逻辑(简化版)
library SafeMath {
    function add(uint256 a, uint256 b) internal pure returns (uint256) {
        uint256 c = a + b;
        require(c >= a, "SafeMath: addition overflow");
        return c;
    }
    
    function sub(uint256 a, uint256 b) internal pure returns (uint256) {
        require(b <= a, "SafeMath: subtraction overflow");
        return a - b;
    }
    
    function mul(uint256 a, uint256 b) internal pure returns (uint256) {
        if (a == 0) return 0;
        uint256 c = a * b;
        require(c / a == b, "SafeMath: multiplication overflow");
        return c;
    }
}

我在项目中遇到过最典型的用法是这样的:

// 使用SafeMath的NFT合约
import "@openzeppelin/contracts/math/SafeMath.sol";

contract MyNFT is ERC721 {
    using SafeMath for uint256;
    
    uint256 public totalSupply;
    
    function mint(address to) external {
        uint256 tokenId = totalSupply.add(1); // 安全加法
        _safeMint(to, tokenId);
        totalSupply = tokenId;
    }
}

你看,用.add()代替+,用.sub()代替-。就这么简单,但能避免90%的溢出问题。

💡 我的建议:即使你用的是Solidity 0.8+,如果项目中涉及复杂的数学运算(比如价格计算、比例计算),我仍然建议使用SafeMath或类似的检查库。因为0.8只检查加减乘除的基本溢出,不检查除法精度丢失等问题。

3. NFT合约中的数量计算陷阱

NFT合约里最容易出问题的,不是简单的加减法,而是批量操作中的累加逻辑

我曾经审计过一个项目,它的批量铸币函数是这样的:

// 有漏洞的批量铸币
function batchMint(address to, uint256 amount) external {
    require(amount > 0);
    require(amount <= 100); // 单次最多100个
    
    uint256 currentSupply = totalSupply();
    for (uint256 i = 0; i < amount; i++) {
        _safeMint(to, currentSupply + i); // 这里没问题
    }
    // 但totalSupply的更新呢?没有!
}

这个例子的问题不是溢出,而是totalSupply没有更新。但更隐蔽的陷阱是:

// 更隐蔽的溢出陷阱
function batchMintV2(address[] memory recipients, uint256[] memory amounts) external {
    uint256 totalAmount = 0;
    for (uint256 i = 0; i < recipients.length; i++) {
        totalAmount += amounts[i]; // 如果recipients.length很大,这里可能溢出
    }
    require(totalAmount <= maxSupply);
    // ... 后续铸币逻辑
}

嗯,这里要注意:totalAmount += amounts[i]这个累加,如果recipients数组有几百个元素,每个amount是10^18,累加起来很容易溢出。虽然uint256很大,但理论上存在风险。

🔍 实战避坑指南:
  • 所有涉及+=-=的地方,都要考虑溢出
  • 批量操作中的累加变量,建议用SafeMath或0.8的checked模式
  • 铸币数量计算:totalSupply + mintAmount一定要检查
  • 转账余额计算:balance + transferAmount也要检查

4. 0.8之后的解决方案

Solidity 0.8.0开始,编译器默认启用了算术检查。也就是说,溢出会自动revert。但要注意:

  • 只在unchecked块内才会忽略溢出检查
  • 如果你用unchecked,要自己保证不会溢出
  • 某些场景下(比如循环计数器),用unchecked可以省gas
// 0.8+ 的安全写法
function safeMint(address to, uint256 amount) external {
    uint256 newSupply;
    unchecked {
        // 这里故意用unchecked,因为我们已经检查了上限
        newSupply = totalSupply + amount;
    }
    require(newSupply <= maxSupply, "Exceeds max supply");
    totalSupply = newSupply;
    // ... 铸币逻辑
}
⚠️ 重要提醒:不要因为用了0.8就掉以轻心。我见过有人把unchecked用在整个函数上,结果里面的所有运算都不检查了。正确的做法是:只在确定不会溢出的地方用unchecked,其他地方保持默认检查。

5. 知识体系图

下面这张图总结了整数溢出漏洞的核心知识点:

整数溢出漏洞知识体系 溢出原理 • 0.8之前:静默溢出 • 0.8之后:默认检查 • uint8溢出示例 • uint256理论风险 • unchecked块的使用 SafeMath库 • add() 安全加法 • sub() 安全减法 • mul() 安全乘法 • require检查机制 • 0.8+仍可选用 NFT数量陷阱 • 批量铸币累加 • totalSupply更新 • 余额计算陷阱 • 循环计数器溢出 • 数组长度累加 核心防御策略 1. 使用Solidity 0.8+ 并保持默认算术检查 2. 复杂运算仍推荐SafeMath或自定义检查 3. 批量操作中的累加变量必须检查溢出

6. 总结

整数溢出,说白了就是数字“绕了一圈又回来了”。在NFT合约里,这可能导致铸币数量失控、余额计算错误,甚至让攻击者免费铸造大量NFT。

我个人习惯是:能用0.8就用0.8,能用SafeMath就用SafeMath。不要觉得“uint256很大,不会溢出”,在复杂的业务逻辑里,什么都有可能发生。

我曾经因为一个循环累加没检查,导致测试网上的NFT数量翻了一倍。虽然只是测试网,但那次教训让我养成了“每个加法都要检查”的习惯。

📌 最后的小贴士:审计自己的合约时,可以专门搜索+=-=++--这些操作符。每一个都问自己一句:“这里会不会溢出?” 如果答案是“可能”,那就加上检查。

公众号:蓝海资料掘金营,微信deep3321