【得得技术指南】ERC20 Token智能合约六类常见漏洞
摘要: 智能合约掌握着巨额的经济价值,其影响力之大,波及范围之广可见一斑。哪怕一个很不起眼的小问题,都有可能造成不可挽回的经济损失。
前不久,轰动一时的BEC事件在币圈和链圈掀起了不小的反响,智能合约安全一度成为焦点话题。
通过一个小小的整数溢出漏洞,黑客盗取了巨量的BEC token,迫使交易所不得不暂时停止交易。一时间BEC价格呈现断崖式下跌。随后,SMT token 的智能合约也因为相同的问题遭到了攻击。两次事件都给项目发行方和token持有者造成了巨额的损失。
据统计,截止2018年5月12日为止,以太坊上部署的合约总量共计 1628059个,其中ERC20合约数量为 71233 个,占比 4.3%。这些合约所代表的经济价值更是难以估量。
上图反映了自以太坊平台诞生以来,ERC20代币合约的创建情况,整体呈现增长趋势。自2017年6月开始便持续走高,而2018年的增长幅度尤为明显,平均每日合约创建数约为320 个,尤其在今年三月份,一度达到峰值,一天内创建的合约数量更是超过600个。随着区块链热度的进一步加强,越来越多的区块链项目也在萌芽,相信2018年新合约的数量还会逐步上升。
SECBIT实验室通过查询公开数据表明,大量已经部署的智能合约或多或少都存在着一定的安全风险和漏洞,BEC事件也只是冰山一角。那么就ERC20合约而言,除了整数溢出漏洞以外,还有可能面临哪些风险呢?
1.可重入
若一个程序或子程序可以「在任意时刻被中断然后操作系统调度执行另外一段代码,这段代码又调用了该子程序不会出错」,则称其为可重入(reentrant或re-entrancy)的。
在智能合约代码中,黑客可以利用fallback函数,来递归调用包含 call.value() 的函数 ,从合约中重复提取以太币。通常造成该类事件的原因是余额校验不到位或余额更新不及时。
当ERC20代币合约中涉及以太币的转出,应当尤为注意。以一个 withdraw 函数为例,这也是ERC20合约中经常出现的函数。以下代码中首先进行余额校验,并向msg.sender地址转出以太币,再修改 balance 数组中余额值。当函数执行到 msg.sender.call.value(amount)() 时,黑客就可以通过msg.sender的 fallback函数来重复调用 withdraw,进而重复执行 msg.sender.call.value(_amount)(),直到gas全部消耗完毕或者合约中的以太余额全部被取完。于是就给黑客实现 reentrancy 攻击创造了条件。
function withdraw(uint _amount) public {
if(balances[msg.sender] >= _amount) {
if(!msg.sender.call.value(_amount)()) {
throw;
}
balances[msg.sender] -= _amount;
}
}
因此,SECBIT实验室的工程师强烈建议智能合约开发者在转账之前做好余额校验工作,并且将余额计算放在转账之前处理。
function withdraw(uint _amount) public {
if(balances[msg.sender] >= _amount) {
balances[msg.sender] -= _amount;
if(!msg.sender.call.value(_amount)()) {
throw;
}
}
}
另外,值得重视的是,除了 call.value() 以外,任何直接或者间接调用call方法的步骤,都有可能引起回调,从而引发重入的安全事件。
2.转账方式风险
当然,杜绝上述问题的一个更好的方式就是不使用 call.value() 进行转账。发起以太币转账的方式有三种transfer() ,send() 和 call.value()。我们对这三种方式进行比较。
如上图所示,其中最安全的方式当属 transfer(),一旦转账失败,transfer() 会抛出异常直接触发 revert() 事件,而另外两者不会,需要开发人员手动处理返回值。send() 与 transfer() 唯一的区别也在于返回值,通常我们可以认为addr.transfer(v) 就相当于require(addr.send(v))。
而call.value() 与另外两者一个最明显的区别在于gas的限制上面,call.value()允许消耗掉所有的gas。但另外两种方式由于gas消耗限制到2300,不足以完成递归调用,这也是能够避免 reentrancy 攻击的原因,上述 withdraw 的代码可以按照以下的写法来实现。
function withdraw(uint _amount) public {
if(balances[msg.sender] >= _amount) {
balances[msg.sender] -= _amount;
msg.sender.transfer(_amount);
}
}
所以,SECBIT实验室的工程师建议开发人员,在ERC20合约开发过程中,如果遇到以太币转出的情况,如非必要,尽量选择使用transfer()函数来完成。
3.避免混淆 tx.origin 和 msg.sender
solidity提供了两种标准的方式来获取合约调用方的地址,tx.origin 和 msg.sender,但是这两者的含义是不同的,其中 msg.sender 是指直接调用当前合约的调用方地址,tx.origin 是指发起本次调用的起始调用方地址。
比如合约(或外账户地址)A去调用合约B,合约B调用合约C。此时在合约C中读取到的 msg.sender 即为合约B的地址,tx.origin 即为A的地址。
若从一个地址直接对合约发起调用,那么 msg.sender 和 tx.origin 是一样的,否则这两个地址就不一样。由于合约无法决定外部调用的关系,开发人员又往往容易混淆两者的含义,进而留下隐患。
以两段真实的ERC20合约为例,第一段将 tx.origin 地址设为合约的owner地址,第二段将 msg.sender 设为合约的owner地址。在智能合约开发过程中,一定先搞明白代码的实现意图,再选择使用 tx.origin 还是 msg.sender,使用 tx.origin 的时候要尤为慎重。
function Ownable() public {
owner = tx.origin;
}
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
function Ownable() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
4.依赖时间戳或者块高度
在 solidity 中,允许获取当前时间戳(或者说交易所在区块的区块高度)。但是,这并不是安全的。一方面,时间戳是打包交易时候由矿工设置的,存在一定的人为操作因素在里面,矿工完全可以对时间戳做轻微的改动;另一方面,我们不能完全排除以太坊未来会在出块时间上做出调整的可能性,因此通过块高度来预估时间是存在一定隐患的。
uint public startTime = 1507032000;
uint public endTime = 1517659200;
function purchase() whenNotPaused payable {
require(!crowdsaleFinished);
require(now >= startTime && now < endTime);
...
}
上述例子中,通过块高度来限制ERC20代币购买的时间段,假如在合约发布后,购买结束前的时间段内,以太坊平台的出块时间做出了调整,那么购买时效也会发生变动。
另外,千万不要使用这两个值来产生随机数。因为在合约外部一定范围内(即同一个区块内)是可以获取到时间戳和块高度的,所以对于同一个区块中的合约来说,这两个值就变得不随机了,这便给黑客留下了可乘之机。
5.整数相除
在solidity中,整数相除遵循向下取整的原则。因此,当遇到不能整除的情况时,一定要谨慎处理。
function div(uint256 a, uint256 b) internal constant returns (uint256) {
uint256 c = a / b;
return c;
}
如上述代码所示,计算结果的关系是`a == b * c + a % b`,而并非`a == b*c`。在ERC20合约开发过程中,经常会遇到使用除法的场景,要当心除法取整的问题,避免引起数据前后不一致的麻烦。
6.关键字过时
随着solidity不断的升级更新,老版本上的一些用法也逐渐被标记为过时然后废弃掉。因此在合约开发和升级过程中,一定要当心过时的用法,避免造成不必要的损失。下表展示了部分过时用法和其替代用法可供参考。
智能合约掌握着巨额的经济价值,其影响力之大,波及范围之广可见一斑。哪怕一个很不起眼的小问题,都有可能造成不可挽回的经济损失,这对大多数的项目来说无疑是灭顶之灾。因此,智能合约的开发一定要慎之又慎,不要忽略任何细枝末节。
另外,强烈建议开发团队在合约发布之前,寻求专业的智能合约审计团队,对合约代码进行安全审计,杜绝隐患。值得一提的是,作为专业的智能合约安全审计团队,SECBIT实验室着力打造高效可靠的安全审计服务,并且利用静态分析工具,实现了对数十个风险点的自动检测。
以上分析及数据均由SECBIT实验室提供。
评论(0)
Oh! no
您是否确认要删除该条评论吗?