智能合约审计之错误使用随机数

文章前言

智能合约中可能需要使用随机数,虽然Solidity提供的函数和变量可以访问明显难以预测的值,如block.number和block.timestamp,但是由于它们公开透明而且可预知且容易受到矿工的影响,即这些随机数在一定程度上是可预测的,所以恶意用户通常可以复制它并依靠其不可预知性来攻击该功能,常见的区块参数如下:

  • block.blockhash(uint blockNumber) returns (bytes32):指定区块的区块哈希,仅可用于最新的256个区块且不包括当前区块,而 blocks从0.4.22版本开始已经不推荐使用,由blockhash(uint blockNumber)代替
  • block.coinbase (address): 挖出当前区块的矿工地址
  • block.difficulty (uint): 当前区块难度
  • block.gaslimit (uint): 当前区块 gas 限额
  • block.number (uint): 当前区块号
  • block.timestamp (uint): 自 unix epoch 起始当前区块以秒计的时间戳
  • now (uint): 目前区块时间戳(block.timestamp)

漏洞演示

示例代码如下:

function rand() public returns(uint256) {
    uint256 random = uint256(keccak256(block.blockhash(block.number)));
    return  random%10;
 }

上述代码使用BlockHash作为随机数,BlockHash在区块正式生成之前是不可知的,在这里通过block.number变量可以获取当前区块区块高度,但是在执行时,当前区块属于未来区块,它的blockhash是不可知的,即只有在打包包含此合约调用的交易时,这个未来区块才变为当前区块,所以合约才可以获取此区块的区块哈希,因此这种调用方式会导致结果永恒为0,而一些合约开发者曲解了 block.blockhash(block.number)的含义,误认为当前区块的区块哈希在运行过程中是已知的,并将之做为随机数来源,因此很不幸,以上的rand函数将永远返回0,很显然会有严重的安全问题,那么是否可以使用当前的区块的前一个区块呢?即:

uint256 random = uint256(keccak256(block.blockhash(block.number - 1)));

这样的方式,虽然理论上可以获得随机数,但这个随机数是不安全的,因为攻击者可以使用改造后的FullNode,让这笔交易可以在FullNode上执行,并获得结果后,再选择性广播那些可以符合攻击者期望的交易,即可以操纵交易的执行结果!

采用未来区块的blockhash可以算得上是一个比较好的方式,即采取两步交易进行随机数的生成:

  • 第一笔交易:触发合约,合约存储某个未来区块高度
  • 第二笔交易:合约检索当前区块高度,如果超过了存储的未来区块高度,则通过区块哈希获得伪随机数结果,不过这种方式也有它的局限性,在TVM中blockhash被限定为只能获取近256个高度区块的数据,因此在以上的两笔交易间隔超过256 * 3s,大约12.8分钟后,这种方式就会失效

防御措施

Oraclize

Oraclize定位为去中心化应用的数据搬运工,它作为Web APIs和DApp的可靠链接,有了Oraclize,就不需要建立额外的信任链,因为我们的行为已经被强制加密验证。

Oraclize是一个可证明的诚实的预言机服务,可以让智能合约访问互联网,Oraclize是平台无关的,为所有主流的智能合约平台提供一种虚拟的接口,通过Oraclize投入大量有意义的数据到区块链中,可以使得智能合约产业更加繁荣,让更多有价值的应用呈现更大的生命力,Oraclize的使用方式可以参考下面的代码:

https://github.com/oraclize/ethereum-examples/blob/master/solidity/random-datasource/randomExample.sol

/*
   Oraclize random-datasource example

   This contract uses the random-datasource to securely generate off-chain N random bytes
*/

pragma solidity ^0.4.11;

import "github.com/oraclize/ethereum-api/oraclizeAPI.sol";

contract RandomExample is usingOraclize {
    
    event newRandomNumber_bytes(bytes);
    event newRandomNumber_uint(uint);

    function RandomExample() {
        oraclize_setProof(proofType_Ledger); // sets the Ledger authenticity proof in the constructor
        update(); // let's ask for N random bytes immediately when the contract is created!
    }
    
    // the callback function is called by Oraclize when the result is ready
    // the oraclize_randomDS_proofVerify modifier prevents an invalid proof to execute this function code:
    // the proof validity is fully verified on-chain
    function __callback(bytes32 _queryId, string _result, bytes _proof)
    { 
        // if we reach this point successfully, it means that the attached authenticity proof has passed!
        if (msg.sender != oraclize_cbAddress()) throw;
        
        if (oraclize_randomDS_proofVerify__returnCode(_queryId, _result, _proof) != 0) {
            // the proof verification has failed, do we need to take any action here? (depends on the use case)
        } else {
            // the proof verification has passed
            // now that we know that the random number was safely generated, let's use it..
            
            newRandomNumber_bytes(bytes(_result)); // this is the resulting random number (bytes)
            
            // for simplicity of use, let's also convert the random bytes to uint if we need
            uint maxRange = 2**(8* 7); // this is the highest uint we want to get. It should never be greater than 2^(8*N), where N is the number of random bytes we had asked the datasource to return
            uint randomNumber = uint(sha3(_result)) % maxRange; // this is an efficient way to get the uint out in the [0, maxRange] range
            
            newRandomNumber_uint(randomNumber); // this is the resulting random number (uint)
        }
    }
    
    function update() payable { 
        uint N = 7; // number of random bytes we want the datasource to return
        uint delay = 0; // number of seconds to wait before the execution takes place
        uint callbackGas = 200000; // amount of gas we want Oraclize to set for the callback function
        bytes32 queryId = oraclize_newRandomDSQuery(delay, N, callbackGas); // this function internally generates the correct oraclize_query and returns its queryId
    }
    
}

考虑一个提供打赌的智能合约,用户调用打赌的接口,这个接口会把用户的请求存储起来,然后调用Oracle随机数生成服务,然后通过Oracle回调服务,判断随机数是否大于某个值,如果成立,那么用户成功,否则用户失败,这就是典型的Oracle的使用案例。

Randao

randao(https://github.com/randao/randao)是一个DAO(去中心化的匿名组织)允许任何人加入,随机数由所有参与者一起合作生成,首先我们需要在区块链上创建一个RANDAO的智能合约,合约定义了参与规则,然后生成随机数的基本过程可以分为下面三个步骤:

  • 第一步:收集有效的sha3(s):参与随机数生成的参与者,首先需要在一个指定的时间区间(比如6个区块的区间,大约72秒)发送m ETH作为抵押到智能合约C,同时发送一个sha3(s)的值到智能合约C ,s是一个只有参与者自己知道的数字
  • 第二步:收集有效的s,在第一步结束后,那些提交了sha3(s)的参与者需要在指定的时间区间内发送s到智能合约C,智能合约C会检查sha3(s)和之前提交的值是否相同,相同的s会被保存到种子集合用来最终生成随机数。
  • 第三步:计算随机数并退回抵押和奖金,在所有的秘密数字s被成功收集后,智能合约C会使用函数f(s1,s2,...,sn)来计算随机数,随机数的结果会写入智能合约的存储,而且结果会被发送到所有之前请求随机数的其他智能合约上面,智能合约C会把第一阶段的抵押返回给参与者,然后奖金会被分成同等分发送给所有的参与者,奖金来源于请求随机值的其他智能合约。

RNG补充规则:

为了确保RNG不能被操控,以及为了安全和效率,智能合约C有以下的补充规则:

  • 在第一步中,如果有两个或更多个的同样的sha3(s)被提交上来,那么只有第一个会被接受
  • 在第一步中,对于参与者有最低要求,如果在指定时间区间内没有收集到足够多的sha3(s)的值,那么RNG在这个区块高度会失败
  • 如果参与者提交了sha3(s),那么他必须在第二步提交s
    • 如果参与者在第二步没有提交s,那么第一阶段提供的m ETH会被没收而且没有奖励
    • 如果一个或者多个s没有在第二步被提交,RNG在这个区块高度会失败,没收的ETH会被分成同等分发送给提交了s的其他参与者,其他申请随机数的其他合约的费用会被退回

RNG激励机制:

RNG的周期非常短,例如一个小时20个生成周期,如果没有周期的利润是0.001%,一个月的盈利会达到0.00001 * 20 * 24 * 30 = 0.144,为了达到14.4%每个月的盈利,并且RNG平均有n个参与者,运行智能合约C的费用为n * 3 * 500 * gasPrice + Ccost,CCost是合约内部的gas消费,包括计算和存储)假设每个随机值平均有r个请求,每个请求的费用是p ETH, 那么收入是r*p. 所以每个参与者每一次参与会收到rp - 1500n * gasPrice - Ccost)/n,当前的gasPrice是10 szabo, 合约的消费大概是1500n gas, 所以大概的净收入是(rp/n-0.03)ETH. 假设每个RNG有10个参与者,并且抵押是1000ETH,所以如果RNG如果只请求一次,那么一次的费用是0.4 ETH, 如果请求是10次,那么一次请求的价格会被降到0.04ETH

RANDAO作为以太坊系统的基础设施,被其他的合约调用,不同的合约因为有不同的目的所以需要不同的随机值,有些需要高度加密的,比如说抽奖;有些需要稳定的回应,并且要求立即作出回应,这些合约本身的价值不高;有些需要回调函数,当随机值已经生成的时候需要接收到通知。

参考链接

https://github.com/oraclize/ethereum-examples/blob/master/solidity/random-datasource/randomExample.sol

https://gitee.com/ywbrj042/go-ethereum-code-analysis/blob/master/%E4%BB%A5%E5%A4%AA%E5%9D%8A%E9%9A%8F%E6%9C%BA%E6%95%B0%E7%94%9F%E6%88%90%E6%96%B9%E5%BC%8F.md

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

FLy_鹏程万里

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值