我们将带大家了解智能合约中一个经常被用到的东西——随机数。

智能合约的开发中常常会用到随机数,例如 Lottery 和现在流行的 NFT 数字藏品的属性等都需要用到随机数。目前来说常见的随机数获取有两种:使用区块变量生成随机数,使用预言机来生成随机数。下面我们了解一下这两者的特点:

 

1使用区块变量生成随机数

 

我们先了解一下常见的区块变量有哪些:

block.basefee(uint):当前区块的基本费用

block.chainid(uint):当前链 id

block.coinbase():当前区块矿工地址 address payable

block.difficulty(uint):当前区块难度

block.gaslimit(uint):当前区块 gaslimit

block.number(uint):当前区块号

block.timestamp(uint): Unix 纪元以来的当前区块时间戳(以秒为单位)

blockhash(uint blockNumber) returns (bytes32):给定区块的哈希,仅适用于 256 个最近的区块

 

其中 block.difficulty, blockhash, block.number 和 block.timestamp 这四个是用得比较多的。由区块数据生成的随机数可能会限制普通用户预测随机数的可能性,但是并不能限制矿工作恶,矿工可以决定一个区块是否被广播,他们挖出了一个区块不是一定要广播出去也可以直接扔掉,这个就叫矿工的选择性打包。他们可以持续尝试生成随机数,直至得到想要的结果再广播出去。当然,矿工会这样做的前提是有足够的的利益诱惑,例如可以获得一个很大的奖励池中的奖励,因此使用区块变量获取随机数的方法更适合于一些随机数不属于核心业务的应用。

 

2)使用预言机生成随机数

 

预言机是专门为生成随机数种子而搭建的链上或者链下的服务。除了使用第三方服务,也可以由 DApp 开发商自己搭建一个链下服务提供随机数,这种在链上获取链下数据的场景通常是通过链上预言机的方式来实现。

 

当然这种方法也会有一些安全风险,例如依赖第三方给出的随机数种子的话同样会存在第三方作弊或者受贿的情形,即使是自己搭建的随机数服务也可能因为故障等原因无法使用,项目方也有可能操控随机数对 DApp 的运行和用户造成重大的损失。因此使用链下服务获取随机数的方法依赖于是否有一个可信又稳定的第三方服务,如果有,那么这个方法相较于使用区块链变量生成随机数的方法,随机数的不可预测性会更强一些。

 

接下来我们还是用合约代码来给大家演示弱随机数可能带来的危害。

 

漏洞示例

 

智能合约安全——随机数 

 

漏洞分析

 

首先我们先来了解一下代码中的两个函数,abi.encodePacked 和 keccak256:

 

 

abi.encodePacked 对参数进行编码,solidity 提供两种编码方法 encode 和  encodePacked,前者对每一个参数进行 32 字节补齐,后者不进行补齐而是直接将待编码参数连接起来。

 

 

keccak256 哈希算法,可以将任意长度的输入压缩成 64 位的 16 进制的数,且哈希碰撞的概率近乎为 0。

 

 

接下来我们来看合约代码,这个合约是一个猜数字赢以太的游戏,我们可以看到,部署者使用上个区块的区块哈希和区块时间作为随机数种子生成随机数,我们只需要模拟他的随机数生成方法就可以得到奖励。下面我们来看攻击合约:

 

攻击合约

智能合约安全——随机数 

 

下面我们先来分析攻击流程:

 

攻击者调用Attack.attack()函数,它模拟了 GuessTheRandomNumber 合约中随机数的生成方式生成随机数后调用 guessTheRandomNumber.guess() 并将生成的随机数传入,由于从 Attack.attack() 生成随机数到调用 guessTheRandomNumber.guess() 都是在同一区块中完成的,且在同一区块中 block.number 和 block.timestamp 这两个参数是不变的,所以,Attack.attack() 和 guessTheRandomNumber.guess() 这两个函数生成的随机数的结果是相同的,从而攻击者可以顺利通过 if(_guess == answer) 判断得到奖励。

 

修复建议

 

如果随机数属于非核心业务的话可以使用未来区块哈希来生成随机数也就是将猜数和领奖分开做异步处理。针对这次的漏洞合约写一个优化版本,大家可以看下:

智能合约安全——随机数 

添加了deadline 参数将 guess 和 claim 做异步处理,在部署合约后的 72 小时内可以调用 guess() 猜随机数,在 72 小时后 guess() 关闭 claim() 开启,玩家可以通过 claim() 来验证自己是否猜中。当然,这个修复合约并不是完美的解决方案,正如前置知识中提到的,如果矿工来玩的话他可以在打包的时候知道自己是否猜中,如果猜中打包上链,如果没有猜中放弃打包(相信没有任何一个矿工愿意为了得到一个以太而付出这么大的代价)。所以最优的解决办法还是接入知名预言机来获取随机数。

 

如果想了解更多的智能合约和区块链知识,欢迎到区块链交流社区CHAINPIP社区,一起交流学习~社区地址:https://www.chainpip.com/