智能合约开发必读:ERC20 Token合约你可能不知道的坑
发布时间 2018-05-23 14:55 全姚 阅读 1476次

前不久,轰动一时的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合约而言,除了整数溢出漏洞以外,还有可能面临哪些风险呢?

可重入

若一个程序或子程序可以「在任意时刻被中断然后操作系统调度执行另外一段代码,这段代码又调用了该子程序不会出错」,则称其为可重入(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方法的步骤,都有可能引起回调,从而引发重入的安全事件。

转账方式风险

当然,杜绝上述问题的一个更好的方式就是不使用 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()函数来完成。

避免混淆 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); _;}

依赖时间戳或者块高度

在 solidity 中,允许获取当前时间戳(或者说交易所在区块的区块高度)。但是,这并不是安全的。一方面,时间戳是打包交易时候由矿工设置的,存在一定的人为操作因素在里面,矿工完全可以对时间戳做轻微的改动;另一方面,我们不能完全排除以太坊未来会在出块时间上做出调整的可能性,因此通过块高度来预估时间是存在一定隐患的。

uint public startTime = 1507032000; uint public endTime = 1517659200; function purchase() whenNotPaused payable { require(!crowdsaleFinished); require(now >= startTime && now < endTime); ...}

上述例子中,通过块高度来限制ERC20代币购买的时间段,假如在合约发布后,购买结束前的时间段内,以太坊平台的出块时间做出了调整,那么购买时效也会发生变动。

另外,千万不要使用这两个值来产生随机数。因为在合约外部一定范围内(即同一个区块内)是可以获取到时间戳和块高度的,所以对于同一个区块中的合约来说,这两个值就变得不随机了,这便给黑客留下了可乘之机。

整数相除

在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合约开发过程中,经常会遇到使用除法的场景,要当心除法取整的问题,避免引起数据前后不一致的麻烦。

关键字过时

随着solidity不断的升级更新,老版本上的一些用法也逐渐被标记为过时然后废弃掉。因此在合约开发和升级过程中,一定要当心过时的用法,避免造成不必要的损失。下表展示了部分过时用法和其替代用法可供参考。

智能合约掌握着巨额的经济价值,其影响力之大,波及范围之广可见一斑。哪怕一个很不起眼的小问题,都有可能造成不可挽回的经济损失,这对大多数的项目来说无疑是灭顶之灾。因此,智能合约的开发一定要慎之又慎,不要忽略任何细枝末节。

另外,强烈建议开发团队在合约发布之前,寻求专业的智能合约审计团队,对合约代码进行安全审计,杜绝隐患。值得一提的是,作为专业的智能合约安全审计团队,SECBIT实验室着力打造高效可靠的安全审计服务,并且利用静态分析工具,实现了对数十个风险点的自动检测。

以上分析及数据均由SECBIT实验室提供。合作交流请联系 info@secbit.io。

SECBIT实验室由一群热爱区块链技术的极客组建,成员遍布在全球多个国家,专业领域涉及区块链底层架构、智能合约语言、形式化验证、密码学与安全协议、编译与分析技术、博弈论与加密经济学等诸多学科。SECBIT实验室目前聚焦智能合约的安全问题,助力区块链项目团队提高智能合约的可靠性与安全性,开展智能合约安全框架的理论技术研发。SECBIT实验室致力于参与共建共识、可信、有序的区块链经济体。

半导体行业并购升温 产业整合提速 [原文链接]
英伟达季度业绩超预期 部分投资者获利回吐[原文链接]
5G与工业互联网融合不是简单相加[原文链接]
电商平台“双11”从拼数据转向拼内力[原文链接]
我国已建近万家数字化车间和智能工厂[原文链接]
每周精选查看更多 >
希鸥网观点:创业公司团队如何管理更有效率?
希鸥网观点:创业公司团队如何管理更有效率?
当团队中出现冲突或挑战时,积极介入并提供协助和支持。促进团队成员之间的沟通和解决问题的能力,以保持团队的凝聚力和效率。 [详细]
想升职加薪?拿着超4亿年薪的CEO给了20条建议
想升职加薪?拿着超4亿年薪的CEO给了20条建议
我们熟知的“迪士尼”,全称是华特迪士尼公司(英文简称:DISNEY),作为一家市值超过2万亿人民币(3103亿美金)的大型企业,迪士尼最为国人所熟悉的是其位于上海的迪士尼乐园和电影屏幕上的公主系列大电影,但这些只是迪士尼公司的一... [详细]
如果最近你创业不顺,不妨读一读段永平这100句话
如果最近你创业不顺,不妨读一读段永平这100句话
上市后,拼多多市值一度超过京东,在所有中国互联网企业中排名第四。因此,其“80后”CEO黄峥也被人们戏称为“杭州80后新首富”、“抛弃你的同龄人。... [详细]
希鸥网李志磊:创业5年认识的六个创业真相
希鸥网李志磊:创业5年认识的六个创业真相
2013年7月,大学毕业一周年,遭遇简短创业带来的失败以及工作不顺利,我进入了人生低谷,此后一个月,每天花20块钱买10注双色球,希望可以一夜暴富。一个月后终于中了五块钱,我意识到,翻身不能靠运气,还是要要靠自己的努力... [详细]
知乎创始人周源曾创业失败发不出工资:我哭了,因为不甘心
知乎创始人周源曾创业失败发不出工资:我哭了,因为不甘心
说起知乎,想必大家都不会陌生,但站在知乎背后的男人,大概很少有人会去了解。周源是知乎创始人兼CEO,他自称“知乎第001号员工”。2018年周源做客一档由腾讯大学自制的名为《CEO来了》的节目,谈到自己的创业经历,分享在此。... [详细]