title: S05. 整型溢出 tags: solidity security WTF Solidity 合约安全: S05. 整型溢出 我最近在重新学 Solidity,巩固一下细节,也写一个“WTF Solidity 合约安全”,供小白们使用(编程大佬可以另找教程),每周更新 1-3 讲。 推特:@0xAAScience|@WTFAcademy 社区:Discord|微信群|官网 wtf.academy 所有代码和教程开源在 github: github.com/AmazingAng/WTF-Solidity 这一讲,我们将介绍整型溢出漏洞(Arithmetic Over/Under Flows)。这是一个比较经典的漏洞,Solidity 0.
title: S05. 整型溢出 tags: - solidity - security
我最近在重新学 Solidity,巩固一下细节,也写一个“WTF Solidity 合约安全”,供小白们使用(编程大佬可以另找教程),每周更新 1-3 讲。
所有代码和教程开源在 github: github.com/AmazingAng/WTF-Solidity
这一讲,我们将介绍整型溢出漏洞(Arithmetic Over/Under Flows)。这是一个比较经典的漏洞,Solidity 0.8版本后内置了Safemath库,因此很少发生。
以太坊虚拟机(EVM)为整型设置了固定大小,因此它只能表示特定范围的数字。例如 uint8,只能表示 [0,255] 范围内的数字。如果给 uint8 类型变量的赋值 257,则会上溢(overflow)变为 1;如果给它赋值-1,则会下溢(underflow)变为255。
攻击者可以利用这个漏洞进行攻击:想象一下,黑客余额为0,他凭空花 $1 之后,余额突然变成了 $2^256-1。2018年的土狗项目 PoWHC 因为这个漏洞被盗了 866 ETH。

下面这个例子是一个简单的代币合约,参考了 Ethernaut 中的合约。它有 2 个状态变量:balances 记录了每个地址的余额,totalSupply 记录了代币总供给。
它有 3 个函数:
transfer():转账函数。balanceOf():查询余额函数。由于solidity 0.8.0 版本之后会自动检查整型溢出错误,溢出时会报错。如果我们要重现这种漏洞,需要使用 unchecked 关键字,在代码块中临时关掉溢出检查,就像我们在 transfer() 函数中做的那样。
这个例子中的漏洞就出现在transfer() 函数中,require(balances[msg.sender] - _value >= 0); 这个检查由于整型溢出,永远都会通过。因此用户可以无限转账。
// SPDX-License-Identifier: MIT pragma solidity ^0.8.21; contract Token { mapping(address => uint) balances; uint public totalSupply; constructor(uint _initialSupply) { balances[msg.sender] = totalSupply = _initialSupply; } function transfer(address _to, uint _value) public returns (bool) { unchecked{ require(balances[msg.sender] - _value >= 0); balances[msg.sender] -= _value; balances[_to] += _value; } return true; } function balanceOf(address _owner) public view returns (uint balance) { return balances[_owner]; } }
Remix 复现Token 合约,将总供给设为 100。1000 个代币,可以转账成功。2^256。Solidity 0.8.0 之前的版本,在合约中引用 Safemath 库,在整型溢出时报错。
Solidity 0.8.0 之后的版本内置了 Safemath,因此几乎不存在这类问题。开发者有时会为了节省gas使用 unchecked 关键字在代码块中临时关闭整型溢出检测,这时要确保不存在整型溢出漏洞。
这一讲,我们介绍了经典的整型溢出漏洞,由于solidity 0.8.0 版本后内置 Safemath 的整型溢出检查,这类漏洞已经很少见了。