背景介绍
2018年4月发生的事件,美链(Beauty Chain)是发行在以太坊上的代币,这些代币没有自己的区块链,而是以智能合约的形式运行在以太坊的EVM平台上。发行这个代币的智能合约,对应的是以太坊状态树的一个节点,这个节点有它自己的账户余额,就相当于这个智能合约一共有多少个以太币,就是发行这个代币的智能合约它总的资产有多少个以太币,然后在这个合约里每个账户上有多少个代币,这个是作为存储在树中的变量,存储在智能合约的账户里。代币的发行、转账、销毁都是通过调用智能合约中的函数来实现的,这个也是跟以太坊上的以太币不太一样的地方,他不像以太坊那样需要挖矿来维护底层的基础链,像以太坊上每个账户有多少个以太币,这个直接保存在状态树种的变量,然后以太坊上面两个账户转账是通过发布一个交易到区块链上,这个交易会打包到发布的区块链上面,而代币发生转账的话实际上就是智能合约上面两个账户之间发生转账,通过调用智能合约上的函数,就可以完成了。每个代币都可以指定自己的发行规则,比如某个代币是1个以太坊兑换100个代币,那么比如说从某个外部账户转1个以太币给这个智能合约,这个智能合约就可以给你在这个智能合约里的代币账户上发送100个代币。
以太坊平台的出现给发行代币提供了方便,包括以前说的EOS,这个在上线之前也是作为以太坊上代币形式,上线的意思是有自己的基础链了,不用依附在以太坊上了。以太坊发行代币的标准为ERC20(Ethereum Request for Comments)。
batchTransfer函数的实现
美链中有一个叫batchTransfer的函数,就是一次性向很多个接收者发送代币,然后把这些代币从调用这个函数的账户上扣掉。美链的代币叫BEC,比如我有很多BEC,给10个不同的账户发送代币,调用这个batchTransfer函数,每个人发送100个代币,那么这个batchTransfer函数先从我的账户上扣掉1000个代币,然后给10个账户分别增加100个代币。
batchTransfer函数有两个参数,第一个参数是数组,接受代币者的地址,函数中规定接收者的数目最多是20个,第二个参数value是转账的金额,先算一下总金额amount,recevier的数目和每个接受的代币计算了然后检查一下发起调用的账户msg.sender确实是有这么多代币的。之后把发起账户的代币数目减去amount,下面是一个循环给每一个接收者发送value这么多的代币。
上图红框中的乘法,当value值很大的时候可能会发生溢出,amount算出来可能是个很小的一个值,所以从调用者的代币中减的时候是很小一部分的代币,但还是每个receivers增加那么多value的代币,这样做造成了系统中凭空多发行了很多代币。
攻击细节
上图中一堆数字是函数调用的参数,该函数有两个参数,分别对应那串数字的前两行,第一个参数是地址,第一行给出的实际是第一个参数出现的具体位置,这里是16进制,40也就是64,也就是说第一个参数出现 在第64个字节的位置,每一行是32个字节,所以实际上是从第2号开始出现的。第二行是这个value的值,这是个很大的树,前面是8,后面都是0,第三行是这个数组的具体内容,数组的长度是2,接下来两行是两个接受的地址。
参数的特点:第二行amount是8,再乘以2,算出来的amount恰好溢出为0,add(value)的时候还是加原来特别大的那一串数目。
上图红框中是接收地址接收的代币,每个地址都接收到了很大一部分代币。
攻击使代币的价格造成致命性的打击,差不多快要归零了。
代币上市的交易所在发生攻击后暂停提币的功能,防止黑客携款逃走。两天后回滚交易,这个事件影响没有The DAO大。
反思
在进行数学运算的时候一定要考虑溢出的可能性。Solidity有一个safeMath库,里面提供的操作运算都会自动检测有没有出现溢出。
mul()函数中,先用a * b = c,再用c/a看看是否等于b,如果发生溢出的话,assert()会抛出异常。
C语言里,两个数相乘会有一定的精度损失,再除以一个数,不一定会得到和另外一个数一摸一样的数。但是在Solidity里面是不存在的,因为两个数都是256位的整数,整数先进行乘法,再进行除法。
batchTransfer的加法和减法都用的safeMath库,只有乘法不小心没有使用,结果酿成了悲剧。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。