原理
智能合约编程语言Solidity中,变量支持的整数类型步长以8递增,支持从uint8到uint256,以及int8到int256。一个 uint8类型 ,只能存储在范围 0到2^8-1,也就是[0,255] 的数字,一个 uint256类型 ,只能存储在范围 0到2^256-1的数字。由于uint8到uint256只能表示一定范围的整数,所以是存在溢出问题的。
整数溢出可以细分为整数上溢和整数下溢,以uint8类型为例,两种溢出情况如图所示:
示例
pragma solidity ^0.4.25;
contract IntegerOverflow{
//加法溢出,uint256类型变量达到了最大值(2**256-1),再加上一个1,整数上溢为0
function add_overflow() view returns(uint256){
uint256 max = 2**256 - 1;
return max + 1;
}
//减法溢出,uint256类型变量达到了最小值0,再减去一个1,整数下溢成最大值
function sub_underflow() view returns(uint256){
uint256 min = 0;
return min - 1;
}
//乘法溢出,uint256类型变量超过了(2**256-1),最后会溢出为0
function mul_overflow() view returns(uint256){
uint256 mul = 2**255;
return mul * 2;
}
}
防护
对于整数溢出问题,OpenZeppelin 提供了一套智能合约函数库中的 SafeMath ,该方法可以有效防止整数溢出问题。
pragma solidity ^0.4.25;
library SafeMath{
function mul(uint256 a, uint256 b) internal constant returns (uint256){
uint256 c = a * b;
assert(a == 0 || c / a == b);
return c;
}
function div(uint256 a, uint256 b) internal constant returns (uint256){
uint256 c = a / b;
return c;
}
function sub(uint256 a, uint256 b) internal constant returns (uint256){
assert(b <= a);
return a - b;
}
function add(uint256 a, uint256 b) internal constant returns (uint256){
uint256 c = a + b;
assert(c >= a);
return c;
}
}
contract IntegerOverflow{
using SafeMath for uint256;
//加法溢出,uint256类型变量达到了最大值(2**256-1),再加上一个1,整数上溢为0
function add_overflow() view returns(uint256){
uint256 max = 2**256 - 1;
return max.add(1);
}
//减法溢出,uint256类型变量达到了最小值0,再减去一个1,整数下溢成最大值
function sub_underflow() view returns(uint256){
uint256 min = 0;
return min.sub(1);
}
//乘法溢出,uint256类型变量超过了(2**256-1),最后会溢出为0
function mul_overflow() view returns(uint256){
uint256 mul = 2**255;
return mul.mul(2);
}
}
使用SafeMath后调用合约函数,存在整数溢出的代码执行失败。可见使用SafeMath处理算术逻辑可以有效防止整数溢出。
经典案例
BEC合约整数溢出
2018年4月22日,黑客对BEC智能合约发起攻击,凭空取出巨量BEC代币在市场上进行抛售,BEC随即急剧贬值,价值几乎为0,该市场瞬间土崩瓦解。
BEC合约地址:0xC5d105E63711398aF9bbff092d4B6769C82F793D
BEC代码地址: https://etherscan.io/address/0xc5d105e63711398af9bbff092d4b6769c82f793d#code
恶意交易记录: https://etherscan.io/tx/0xad89ff16fd1ebe3a0a7cf4ed282302c06626c1af33221ebe0d3a470aba4a660f
代码溢出点位于batchTransfer函数中:
function batchTransfer(address[] _receivers, uint256 _value) public whenNotPaused returns (bool) {
uint cnt = _receivers.length; //cnt为转账的地址的数量
uint256 amount = uint256(cnt) * _value; //溢出点,未使用SafeMath进行封装
require(cnt > 0 && cnt <= 20); //规定转账的地址数量范围
require(_value > 0 && balances[msg.sender] >= amount); //单用户转账金额大于0,并且当前用户拥有的代币余额大于等于本次转账的总币数
balances[msg.sender] = balances[msg.sender].sub(amount);//调用SafeMath的sub,从用户余额中减去本次转账花费的币数
for (uint i = 0; i < cnt; i++) { //对转账的地址逐个执行转账操作
balances[_receivers[i]] = balances[_receivers[i]].add(_value);
Transfer(msg.sender, _receivers[i], _value);
}
return true;
}
溢出点为 uint256 amount = uint256(cnt) * _value
,没有使用SafeMath库的mul函数,而是直接采用乘法运算符。通过构造cnt
和_value
的值,可以绕过require(_value > 0 &&balances[msg.sender] >= amount)
语句的判断,导致amount
最终为0。具体赋值为:cnt = _receivers.length = 2
,_value = 2**255
,这样amount = uint256(cnt) * _value = 2**255*2
超过uint256表示的最大值,导致整数上溢,使amount = 0
。
_receivers: ["0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2","0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db"]
_value: 57896044618658097711785492504343953926634992332820282019728792003956564819968
交易后查看两个地址余额,两个地址已经成功接收了2**255个代币,但是原地址的余额并没有发生变化,利用了溢出达到了凭空转账的目的。
对于此处溢出的防护只需要将乘法运算符改成SafeMath库的mul操作即可: uint256 amounnt=uint256(cnt).mul(_value)
至此,整数溢出相关内容了解完毕。为了防止整数溢出的发生,一方面可以在算术逻辑前后进行验证,另一方面可以直接使用 OpenZeppelin 维护的一套智能合约函数库中的 SafeMath 来处理算术逻辑。