原理

智能合约编程语言Solidity中,变量支持的整数类型步长以8递增,支持从uint8到uint256,以及int8到int256。一个 uint8类型 ,只能存储在范围 0到2^8-1,也就是[0,255] 的数字,一个 uint256类型 ,只能存储在范围 0到2^256-1的数字。由于uint8到uint256只能表示一定范围的整数,所以是存在溢出问题的。
整数溢出可以细分为整数上溢和整数下溢,以uint8类型为例,两种溢出情况如图所示:
QQ截图20210122170926.png

示例

  1. pragma solidity ^0.4.25;
  2. contract IntegerOverflow{
  3. //加法溢出,uint256类型变量达到了最大值(2**256-1),再加上一个1,整数上溢为0
  4. function add_overflow() view returns(uint256){
  5. uint256 max = 2**256 - 1;
  6. return max + 1;
  7. }
  8. //减法溢出,uint256类型变量达到了最小值0,再减去一个1,整数下溢成最大值
  9. function sub_underflow() view returns(uint256){
  10. uint256 min = 0;
  11. return min - 1;
  12. }
  13. //乘法溢出,uint256类型变量超过了(2**256-1),最后会溢出为0
  14. function mul_overflow() view returns(uint256){
  15. uint256 mul = 2**255;
  16. return mul * 2;
  17. }
  18. }

0.png

防护

对于整数溢出问题,OpenZeppelin 提供了一套智能合约函数库中的 SafeMath ,该方法可以有效防止整数溢出问题。

  1. pragma solidity ^0.4.25;
  2. library SafeMath{
  3. function mul(uint256 a, uint256 b) internal constant returns (uint256){
  4. uint256 c = a * b;
  5. assert(a == 0 || c / a == b);
  6. return c;
  7. }
  8. function div(uint256 a, uint256 b) internal constant returns (uint256){
  9. uint256 c = a / b;
  10. return c;
  11. }
  12. function sub(uint256 a, uint256 b) internal constant returns (uint256){
  13. assert(b <= a);
  14. return a - b;
  15. }
  16. function add(uint256 a, uint256 b) internal constant returns (uint256){
  17. uint256 c = a + b;
  18. assert(c >= a);
  19. return c;
  20. }
  21. }
  22. contract IntegerOverflow{
  23. using SafeMath for uint256;
  24. //加法溢出,uint256类型变量达到了最大值(2**256-1),再加上一个1,整数上溢为0
  25. function add_overflow() view returns(uint256){
  26. uint256 max = 2**256 - 1;
  27. return max.add(1);
  28. }
  29. //减法溢出,uint256类型变量达到了最小值0,再减去一个1,整数下溢成最大值
  30. function sub_underflow() view returns(uint256){
  31. uint256 min = 0;
  32. return min.sub(1);
  33. }
  34. //乘法溢出,uint256类型变量超过了(2**256-1),最后会溢出为0
  35. function mul_overflow() view returns(uint256){
  36. uint256 mul = 2**255;
  37. return mul.mul(2);
  38. }
  39. }

1.png
使用SafeMath后调用合约函数,存在整数溢出的代码执行失败。可见使用SafeMath处理算术逻辑可以有效防止整数溢出。

经典案例

BEC合约整数溢出

2018年4月22日,黑客对BEC智能合约发起攻击,凭空取出巨量BEC代币在市场上进行抛售,BEC随即急剧贬值,价值几乎为0,该市场瞬间土崩瓦解。
BEC合约地址:0xC5d105E63711398aF9bbff092d4B6769C82F793D
BEC代码地址: https://etherscan.io/address/0xc5d105e63711398af9bbff092d4b6769c82f793d#code
恶意交易记录: https://etherscan.io/tx/0xad89ff16fd1ebe3a0a7cf4ed282302c06626c1af33221ebe0d3a470aba4a660f
7.png
代码溢出点位于batchTransfer函数中:

  1. function batchTransfer(address[] _receivers, uint256 _value) public whenNotPaused returns (bool) {
  2. uint cnt = _receivers.length; //cnt为转账的地址的数量
  3. uint256 amount = uint256(cnt) * _value; //溢出点,未使用SafeMath进行封装
  4. require(cnt > 0 && cnt <= 20); //规定转账的地址数量范围
  5. require(_value > 0 && balances[msg.sender] >= amount); //单用户转账金额大于0,并且当前用户拥有的代币余额大于等于本次转账的总币数
  6. balances[msg.sender] = balances[msg.sender].sub(amount);//调用SafeMath的sub,从用户余额中减去本次转账花费的币数
  7. for (uint i = 0; i < cnt; i++) { //对转账的地址逐个执行转账操作
  8. balances[_receivers[i]] = balances[_receivers[i]].add(_value);
  9. Transfer(msg.sender, _receivers[i], _value);
  10. }
  11. return true;
  12. }

溢出点为 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

  1. _receivers: ["0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2","0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db"]
  2. _value: 57896044618658097711785492504343953926634992332820282019728792003956564819968

3.png4.png
交易后查看两个地址余额,两个地址已经成功接收了2**255个代币,但是原地址的余额并没有发生变化,利用了溢出达到了凭空转账的目的。
5.png6.png
对于此处溢出的防护只需要将乘法运算符改成SafeMath库的mul操作即可:
uint256 amounnt=uint256(cnt).mul(_value)


至此,整数溢出相关内容了解完毕。为了防止整数溢出的发生,一方面可以在算术逻辑前后进行验证,另一方面可以直接使用 OpenZeppelin 维护的一套智能合约函数库中的 SafeMath 来处理算术逻辑。