原理
智能合约编程语言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,整数上溢为0function 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),最后会溢出为0function 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,整数上溢为0function 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),最后会溢出为0function 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 来处理算术逻辑。
