Solidity 游戏/随机数/开宝箱 安全问题

今年靠 Solidity 随机数问题赚了 6 位数,说一下 Solidity 游戏类合约的一个随机数相关的安全问题吧,

随机数

基本上 开箱子(抽卡)、战斗、彩票 等等都跟随机数有关,有些用 block.difficulty、block.number、block.timestamp 等等组个 hash 取模来取随机数的,有使用 ChainLink 等 Oracle 的 真随机合约的。

不管是真随机数还是伪随机数都逃不开数据持久化。

拿抽卡游戏来说,有 R、S、SR、SSR 四种等级吧,然后用户可以用10U来抽卡,抽到的概率是随机的,一般都是用户调用接口,扣过来10U,然后按随机数生成卡片然后将卡片转给用户。那么这个流程中我们可以有什么地方可以操作?

同 transaction 内部

做合约的都知道,我们可以通过合约来抽卡,抽前给自己卡片做个快照,抽后给自己卡片做个快照。一是如果连抽到抽不到都是有概率的我们可以知道有没有抽到,二是如果抽到了,我们可以去读取卡片信息来验证开出来的卡片的稀有度。

如果稀有度没有达到我们的期望值,或者根本没抽到,可以在我们合约这里直接 revert,除了付点 gas 费,我们的 10U 还是老老实实回到我们钱包了。

这个方案需要权衡尝试成本和抽到高级成本,有利润就可以操作

estimateGas

我们广播交易前可以对 call 进行 gas 估算,这个方法比较适用于战斗,比如一个有战斗场景的游戏,战斗成功之后直接发U给你当奖励。如果判定你失败,就不会给你发U,当然成功失败的操作差异不会只有这一个,这样大概率 战斗成功 的 gas 会比 战斗失败 要多一些。

这样在我们调用 fight 之前,就可以进行 gas 估算,如果 gas 比我们算出的中值要高,那么大概率是赢的。

这个方案只能适用于逻辑比较明显,差异明显的合约。对于逻辑复杂的合约,gas是不好估算的,成功率不高

防护方案

以吾之盾,防吾之矛

数据被验证

像上面提到的同交易中利用外部合约验证,验证不通过就 revert 的场景,有几种解决方案。

检查caller的extcodesize

普通用户地址的 extcodesize 一定是 0 的,使用以下代码可以检查是否来自普通用户地址,

contract OnlyForEOA {  
    uint public flag;

    // bad
    modifier isNotContract(address _a){
        uint len;
        assembly { len := extcodesize(_a) }
        require(len == 0);
        _;
    }

    function setFlag(uint i) public isNotContract(msg.sender){
        flag = i;
    }
}

但是,部署合约时在 constructor 中的操作 caller 的 extcodesize 也是 0,所以我们可以使用以下代码 bypass 这种防护方案,这也是为什么这个方案使用删除线的原因

contract FakeEOA {
    constructor(address _a) public {
        OnlyForEOA c = OnlyForEOA(_a);
        c.setFlag(1);
    }
}

tx.origin

检查 tx.origin == msg.sender 是一个较好的方案,它将确保调用者来自普通用户且未经过其他合约来调用,但是

  1. tx.origin 将来有可能 会从以太坊协议中删除
  2. 这种方式将会把合理的合约用户拒之门外,比如多签合约钱包、DAO持有人

所以奶爸更推荐下面的防护方案,在业务逻辑中将 「剧透」 的科学家们拒之门外

延迟读取

比如我在 1000 区块开出卡,我可以记录下来出卡区块,读取稀有度那里可以限定只能 N+1 读取,同区块读取就返回0,这样就完美避免了利用外部合约验证稀有度的问题。

分阶段执行

可以将一个操作拆分为两个阶段,

  1. 第一阶段就隐式的将第二阶段所用到的所有随机数都准备好,不暴露出来。gas都是一模一样的,无法推测、无法验证。
  2. 第二阶段根据第一阶段产生的结果执行对应操作,给对应奖励,这一阶段不论干什么都是无效的,结果已在第一阶段确认。

⚠️ 这里需要限制这两个阶段不能在同一个区块中避免验证,或者直接限制个人地址调用。

估算 Gas

这个问题可以这么解决,战斗成功,奖励 10 U,战斗失败 奖励 0 U 呗,该执行的操作一视同仁,但是你听过一句话吗?「听君一席话,胜听一席话」,给他加 0 U呗,gas 表现出来不能说完全一样(修改数值和不修改数值Gas是不同的),但是差异也是很小的,也减少了特征点。

也可以做些额外操作来补充没进行的操作,类似 花指令

扩展阅读

Comments