从BEC“代币蒸发”事件看智能合约编写注意事项

从BEC“代币蒸发”事件看智能合约编写注意事项_第1张图片
image

昨天朋友圈被一篇文章刷屏,美链(BEC)智能合约的漏洞导致代币价值几乎归零的事件引起整个区块链技术圈的关注(后附原文),今天二师兄带我们一起了解一些智能合约编写的注意事项。

1

Overflow 与 Underflow

Solidity 可以处理 256 位数字, 最高为 2256 - 1, 所以对 (2 256 - 1) 加 1 会导致归 0。同理, 对 unsigned 类型 0 做减 1 运算会得到 (2**256 - 1)

测试代码如下:

pragma solidity 0.4.18;
contract OverflowUnderflow  {
          uint  public zero =  0;
          uint  public max =  2**256  -  1;
          // zero will end up at  2 ** 256 - 1
          function underflow()  public  {
                     zero -=  1;
          }
          function overflow()  public  {
                     max +=  1;
          }
}

尽管他们同样危险, 但是在智能合约中, underflow 造成的影响更大.

比如, 账号 A 持有 X tokens, 如果他发起一笔 X + 1 tokens 的交易, 如果代码不进行校验, 则账号 A 的余额可能发生 underflow 导致余额变多.

可以引入 SafeMath Library 解决:

pragma solidity 0.4.18;
library SafeMath  {
        function mul(uint256 a, uint256 b)  internal pure returns (uint256)  {
                 if  (a==0)  {
                          return  0;
                 }
                 uint c = a * b;
                 assert(c / a == b);
                 return c;
          }
          function div(uint256 a, uint256 b)  internal pure returns (uint256)  {
                   uint256 c = a / b;
                   return c;
          }
          function  sub(uint256 a, uint256 b)  internal pure returns (uint256)  {
                   assert(b <= a);
                   return a - b;
          }
          function add(uint256 a, uint256 b)  internal pure returns (uint256)  {
                   uint256 c = a + b;
                   assert(c >= a);
                   return c;
          }
}
contract OverflowUnderflow  {
          using  SafeMath  for  uint;
          uint  public zero =  0;
          uint  public max =  2  **  256  -  1;
          function underflow()  public  {
                   zero = zero.sub(1);
          }
          function overflow()  public  {
                   max = max.add(1);
          }
}

2

Visibility 与 Delegatecall

Public functions 可以被任意地址调用

External functions 只能从合约外部调用

Private functions 只能从合约内部调用

Internal functions 允许从合约及其子合约调用

External functions 消耗的 gas 比 public 少, 因为其使用 calldata 而 Public 需要复制所有参数到 memory。

3

Delegatecall

Delegatecall is identical to a message call apart from the fact that the code at the target address is executed in the context of the calling contract and msg.sender and msg.value do not change their values. This means that a contract can dynamically load code from a different address at runtime. Storage, current address and balance still refer to the calling contract, only the code is taken from the called address.

这个特性可以用于构建 Library 和模块化代码. 但是与此同时, 这也有可能造成别人对你的代码进行操作。

下例中, 攻击者调用 pwn 方法获得了合约的拥有权。

pragma solidity 0.4.18;
contract Delegate  {
          address public owner;
          function  Delegate(address _owner)  public  {
                   owner = _owner;
          }
          function pwn()  public  {
                   owner = msg.sender;
         }
}
contract Deletagion  {
          address public owner;
          Delegate  delegate;
          function  Delegation(address _delegateAddreses)  public  {
                   delegate  =  Delegate(_delegateAddreses);
                   owner = msg.sender;
          }
          // an attacker can call Delegate.pwn() in the context of Delegation, this means that pwn() will modify the state of **Delegation** and not Delegate, the result is that the attacker takes unauthorized ownership of the contract.
          function  ()  public  {
                   if(delegate.delegatecall(msg.data))  {
                             this;
                    }
          }
}

4

Reentrancy(TheDAO hack)

Solidity 中 call 函数被调用时, 如果带有 value 参数, 则会转发所有他所收到的 gas。

在一下代码片段中, call函数在sender的余额实际减少前被调用。这里有一个漏洞曾导致TheDAO攻击。

function withdraw(uint _amount)  public  {
      if(balances[msg.sender]  >= _amount)  {
          if(msg.sender.call.value(_amount)())  {
             _amount;
          }
          balances[msg.sender]  -= amount;
      }
}

引自Reddit的解释:

In simple words, it’s like the bank teller doesn’t change your balance until she has given you all the money you requested.  “Can I withdraw $500?  Wait, before that , can I withdraw $500?”
And so on.  The smart contracts as designed only check you have $500 at beginning once,  and allow themselves to be interrupted.

附原文:《一行代码蒸发了 ¥6,447,277,680 人民币!》

来源:区块链开发指北

作者:爬虫

文章转载自:CSDN

作者:高金

事件

BEC 智能合约的漏洞爆出,被黑客利用,瞬间套现抛售大额 BEC,60亿在瞬间归零。

而这一切,竟然是因为一个简单至极的程序 Bug。

从BEC“代币蒸发”事件看智能合约编写注意事项_第2张图片
image

背景

今天有人在群里说,Beauty Chain 美蜜代码里面有 bug,已经有人利用该 bug 获得了 57,896,044,618,658,100,000,000,000,000,000,000,000,000,000,000,000,000,000,000.792003956564819968 个 BEC。

那笔操作记录是 0xad89ff16fd1ebe3a0a7cf4ed282302c06626c1af33221ebe0d3a470aba4a660f(https://etherscan.io/tx/0xad89ff16fd1ebe3a0a7cf4ed282302c06626c1af33221ebe0d3a470aba4a660f)

从BEC“代币蒸发”事件看智能合约编写注意事项_第3张图片
image

下面带大家看看,黑客是如何实现的!

我们可以看到执行的方法是 batchTransfer。

那这个方法是干嘛的呢?(给指定的几个地址,发送相同数量的代币)。

整体逻辑是

  • 你传几个地址给我(receivers),然后再传给我你要给每个人多少代币(value);

  • 然后你要发送的总金额 = 发送的人数* 发送的金额;

  • 然后 要求你当前的余额大于 发送的总金额;

  • 然后扣掉你发送的总金额;

  • 然后 给 receivers 里面的每个人发送 指定的金额(value)。

从逻辑上看,这边是没有任何问题的,你想给别人发送代币,那么你本身的余额一定要大于发送的总金额的!

但是这段代码却犯了一个很傻的错!

代码解释

从BEC“代币蒸发”事件看智能合约编写注意事项_第4张图片
image

这个方法会传入两个参数:

  • _receivers

  • _value

_receivers 的值是个列表,里面有两个地址:

0x0e823ffe018727585eaf5bc769fa80472f76c3d7

0xb4d30cac5124b46c2df0cf3e3e1be05f42119033

_value 的值是:

8000000000000000000000000000000000000000000000000000000000000000

我们再查看代码(如下图):

从BEC“代币蒸发”事件看智能合约编写注意事项_第5张图片
image

我们一行一行地来解释:

uint cnt = _receivers.length;

是获取 _receivers 里面有几个地址,我们从上面可以看到 参数里面只有两个地址,所以 cnt=2,也就是 给两个地址发送代币。

uint256 amount = uint256(cnt) * _value;

uint256

首先 uint256(cnt) 是把 cnt 转成了 uint256 类型。那么,什么是 uint256 类型?或者说 uint256 类型的取值范围是多少?

uintx 类型的取值范围是 0 到 2 的 x 次方 -1。也就是,假如是 uint8 的话,则 uint8 的取值范围是 0 到 2 的 8 次方 -1,即 0 到 255。

那么,uint256 的取值范围是:

0 - 2 的 256 次方 -1 ,也就是 0 到 115792089237316195423570985008687907853269984665640564039457584007913129639935

Python 算 2 的 256 次方是多少?

image

那么假如说 设置的值超过了 取值范围怎么办?这种情况称为“溢出”。

举个例子来说明:

因为 uint256 的取值太大了,所以用 uint8 来 举例。

从上面我们已经知道了 uint8 最小是 0,最大是 255。

  • 那么当我 255 + 1 的时候,结果是啥呢?结果会变成 0。

  • 那么当我 255 + 2 的时候,结果是啥呢?结果会变成 1。

  • 那么当我 0 - 1 的时候,结果是啥呢?结果会变成 255。

  • 那么当我 0 - 2 的时候,结果是啥呢?结果会变成 254。

那么,我们回到上面的代码中:

amount = uint256(cnt) * _value

则 amount = 2* _value。

但是此时 _value 是 16 进制的,我们把它转成 10 进制:

(Python 16 进制转 10 进制)

可以看到 _value = 57896044618658097711785492504343953926634992332820282019728792003956564819968

那么 amount = _value*2 = 115792089237316195423570985008687907853269984665640564039457584007913129639936

可以在查看上面看到 uint256 取值范围最大为 115792089237316195423570985008687907853269984665640564039457584007913129639935

此时,amout 已经超过了最大值,溢出 则 amount = 0

下一行代码 require(cnt > 0 && cnt <= 20); require 语句是表示该语句一定要是正确的,也就是 cnt 必须大于 0 且 小于等于 20

我们的 cnt 等于 2,通过!

require(_value > 0 && balances[msg.sender] >= amount);

这句要求 value 大于 0,我们的 value 是大于 0 的 且,当前用户拥有的代币余额大于等于 amount,因为 amount 等于 0,所以 就算你一个代币没有,也是满足的!

balances[msg.sender] = balances[msg.sender].sub(amount);

这句是当前用户的余额 - amount

当前 amount 是 0,所以当前用户代币的余额没有变动。

for (uint i = 0; i < cnt; i++) { balances[_receivers[i]] = balances[_receivers[i]].add(_value); Transfer(msg.sender, _receivers[i], _value);}

这句是遍历 _receivers 中的地址, 对每个地址做以下操作:

balances[_receivers[i]] = balances[_receivers[i]].add(_value); _receivers 中的地址 的余额 = 原本余额+value

所以 _receivers 中地址的余额 则加了 57896044618658097711785492504343953926634992332820282019728792003956564819968 个代币!!!

Transfer(msg.sender, _receivers[i], _value); } 这句则只是把赠送代币的记录存下来!!!

总结

就一个简单的溢出漏洞,导致 BEC 代币的市值接近归 0。

那么,开发者有没有考虑到溢出问题呢?

其实他考虑了,可以看如下的截图:

从BEC“代币蒸发”事件看智能合约编写注意事项_第6张图片
image

除了 amount 的计算外, 其他的给用户转钱都用了 safeMath 的方法(sub,add)。

那么,为啥就偏偏这一句没有用 safeMath 的方法呢。。。

这就要问写代码的人了。。。

啥是 safeMath

从BEC“代币蒸发”事件看智能合约编写注意事项_第7张图片
image

safeMath 是为了计算安全 而写的一个 library。

我们看看它干了啥?为啥能保证计算安全。

function mul(uint256 a, uint256 b) internal constant returns (uint256) {uint256 c = a * b;assert(a == 0 || c / a == b);return c;}

如上面的乘法。他在计算后,用 assert 验证了下结果是否正确!

如果在上面计算 amount 的时候,用了 mul 的话, 则 c / a == b 也就是 验证 amount / cnt == _value。

这句会执行报错的,因为 0 / cnt 不等于 _value。

所以程序会报错!

也就不会发生溢出了...

那么,还有一个小问题,这里的 assert 好 require 好像是干的同一件事 —— 都是为了验证某条语句是否正确!

那么它俩有啥区别呢?

  • 用了 assert 的话,则程序的 gas limit 会消耗完毕;

  • 而 require 的话,则只是消耗掉当前执行的 gas。

总结

那么 我们如何避免这种问题呢?

我个人看法是:

  • 只要涉及到计算,一定要用 safeMath

  • 代码一定要测试!

  • 代码一定要 review!

  • 必要时,要请专门做代码审计的公司来测试代码。

这件事后需要如何处理呢?

目前,该方法已经暂停了(还好可以暂停)所以看过文章的朋友 不要去测试了...

从BEC“代币蒸发”事件看智能合约编写注意事项_第8张图片
image

不过已经发生了的事情咋办呢?

我的想法是,快照在漏洞之前,所有用户的余额情况,然后发行新的 token,给之前的用户发送等额的代币...

本文来源:公众号-程序新视界

作者:二师兄

以下是我们的社区介绍,欢迎各种合作、交流、学习:)

从BEC“代币蒸发”事件看智能合约编写注意事项_第9张图片
image

HiBlock区块链社区更多活动点击“阅读原文”查看

你可能感兴趣的:(从BEC“代币蒸发”事件看智能合约编写注意事项)