Reentrancy-Angriff erklärt: Schutz in Solidity

Reentrancy-Angriffe gehören zu den ältesten – und bis heute profitabelsten – Smart-Contract-Exploits in DeFi. Sie sind leicht zu verstehen, werden schnell versehentlich eingebaut und sind verheerend, wenn sie zentrale Accounting-Pfade wie Withdrawals, Liquidations und Reward-Claims treffen. Selbst erfahrene Teams liefern Reentrancy-Schwachstellen aus, weil sich der Bug oft in „normalen“ External Calls versteckt: ETH senden, Tokens transferieren, ein Vault aufrufen oder mit einem AMM interagieren.

Dieser Artikel erklärt, wie ein Reentrancy-Angriff funktioniert, wie „Smart-Contract-Reentrancy“ in echten Systemen aussieht, warum das Thema auch nach der DAO weiterhin relevant ist, und wie man Reentrancy-Prevention in Solidity mit bewährten Mustern umsetzt (Checks-Effects-Interactions, Pull Payments, ReentrancyGuard und sorgfältiges External-Call-Design). Außerdem verknüpfen wir Reentrancy mit angrenzenden Themen wie flash-loan attacks, delegatecall risks und modernen Solidity Best Practices.

Bei Soken (soken.io) haben wir Hunderte von Production-Contracts aus Lending, Staking, Bridges und Governance geprüft. Reentrancy ist weiterhin eine wiederkehrende Root Cause oder ein beitragender Faktor – insbesondere dann, wenn Protokolle neue Tokens, Hooks, Callbacks oder composable DeFi-Primitives integrieren.


Was ist ein Reentrancy-Angriff (und warum ist er so gefährlich)?

Ein Reentrancy-Angriff entsteht, wenn ein Contract einen External Call ausführt, bevor er seine eigenen State Updates abgeschlossen hat. Dadurch kann der Aufgerufene zurück in die verwundbare Funktion „re-entern“ und eine Aktion – etwa das Abheben von Funds – mehrfach wiederholen.

Reentrancy ist gefährlich, weil Ethereum synchron ist: Wenn dein Contract einen anderen Contract aufruft, springt die Ausführung sofort in den externen Contract – bevor deine Funktion fertig ist. Wenn dein Contract Balances oder Lock-Flags noch nicht aktualisiert hat, kann ein Angreifer genau diesen „Zwischenzustand“ ausnutzen.

Quotable (40–60 words):
Ein Reentrancy-Angriff nutzt aus, dass ein Contract einen External Call ausführt, bevor er internes Accounting abschließt. Da EVM-Calls synchron sind, kann der Callee in die verwundbare Funktion zurückkehren, während der State noch die „alten“ Balances widerspiegelt. So werden wiederholte Withdrawals, Double-Claims oder umgangene Limits möglich – oft wird TVL in einer Transaktion geleert.

Das klassische mentale Modell: „Ich habe zu früh nach außen aufgerufen“

Ein typischer verwundbarer Ablauf sieht so aus:

  1. User ruft withdraw() auf
  2. Contract sendet ETH/Tokens an den User (external call)
  3. Contract aktualisiert balances[msg.sender] (zu spät)
  4. Fallback/receive-Funktion des Angreifers re-entert withdraw(), bevor Schritt 3 ausgeführt wird

Obwohl Solidity und Tooling besser geworden sind, taucht Reentrancy weiterhin auf bei: - ETH-Transfers via call - ERC777-Tokens mit Hooks - ERC721/1155 Safe Transfers (onERC721Received, onERC1155Received) - Vault-Integrationen (ERC4626), Staking-Callbacks, Reward-Distributors - Cross-Contract-„Accounting + Payout“-Mustern - Komplexen DeFi-Flows, bei denen External Calls mitten in der Funktion passieren


Wie funktioniert eine Reentrancy-Schwachstelle in Solidity tatsächlich?

Eine Reentrancy-Schwachstelle liegt vor, wenn eine Funktion (1) einen extern aufrufbaren Pfad hat und (2) eine externe Interaktion ausführt, bevor finale State Changes erfolgen – wodurch ein Re-Entry in eine sensitive Funktion mit veraltetem State möglich wird.

In der Praxis basiert der Exploit auf Control Flow: Du denkst, du sendest Value „am Ende“, aber der externe Contract bekommt die Kontrolle und kann alles tun – inklusive dich zurück aufzurufen.

Quotable (40–60 words):
Eine Reentrancy-Schwachstelle entsteht, wenn eine Solidity-Funktion einen External Call ausführt – ETH senden, Tokens transferieren oder ein anderes Protokoll aufrufen – bevor sie ihren internen State finalisiert. Der Angreifer nutzt Fallback, Token-Hook oder Callback, um die Funktion erneut zu betreten, während Balances und Limits unverändert sind, und wiederholt Withdrawal oder Claim.

Verwundbares Beispiel: ETH-Withdrawal (der „DAO-style“-Bug)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract VulnerableBank {
    mapping(address => uint256) public balance;

    function deposit() external payable {
        balance[msg.sender] += msg.value;
    }

    function withdraw(uint256 amount) external {
        require(balance[msg.sender] >= amount, "insufficient");

        // INTERACTION first (vulnerable)
        (bool ok, ) = msg.sender.call{value: amount}("");
        require(ok, "send failed");

        // EFFECTS last (too late)
        balance[msg.sender] -= amount;
    }
}

Angreifer-Contract, der via receive() re-entert

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

interface IVulnerableBank {
    function deposit() external payable;
    function withdraw(uint256 amount) external;
}

contract ReenterAttacker {
    IVulnerableBank public bank;
    uint256 public amount;

    constructor(address _bank) {
        bank = IVulnerableBank(_bank);
    }

    function attack() external payable {
        require(msg.value > 0, "need ETH");
        amount = msg.value;

        bank.deposit{value: msg.value}();
        bank.withdraw(amount);
    }

    receive() external payable {
        // Re-enter as long as the bank still has ETH
        if (address(bank).balance >= amount) {
            bank.withdraw(amount);
        }
    }
}

Was passiert: Die Bank sendet ETH, receive() des Angreifers läuft, und withdraw() wird erneut aufgerufen, bevor balance[msg.sender] -= amount ausgeführt wird. Der Angreifer leert die Bank.

„Aber ich nutze ERC20-Transfers – bin ich sicher?“

Nicht unbedingt. ERC20 transfer() ruft typischerweise nicht zurück, aber: - Tokens können nicht standardkonform sein (ERC777-ähnliche Hooks oder Proxy-Patterns) - Dein Code könnte einen externen Router, Vault oder Staking-Contract aufrufen - ERC721/1155 Safe Transfers rufen Receiver-Hooks auf - Du nutzt möglicherweise call oder beliebige External Calls in derselben Funktion


Was sind die wichtigsten Arten von Smart-Contract-Reentrancy (und wo tauchen sie in DeFi auf)?

Es gibt mehrere Formen von Smart-Contract-Reentrancy, darunter direkte Funktions-Re-Entries, Cross-Function-Reentrancy und Read-only-Reentrancy – alle relevant für moderne DeFi-Composability.

Quotable (40–60 words):
Reentrancy ist nicht nur „zweimal withdrawen“. Moderne Protokolle sehen direkte Reentrancy (gleiche Funktion), Cross-Function-Reentrancy (Re-Entry in eine andere Funktion mit gemeinsamem State) und Read-only-Reentrancy (Manipulation von Zwischenzuständen für Pricing oder Checks). Jeder External Call vor finalem Accounting kann eine dieser Varianten ermöglichen.

Häufige Reentrancy-Varianten

  1. Single-function reentrancy (klassisch)
    withdraw() wird wiederholt re-entert.

  2. Cross-function reentrancy
    withdraw() ruft nach außen, der Angreifer re-entert claimRewards() oder borrow(), die sich auf gemeinsamen, nur teilweise aktualisierten State stützen.

  3. Read-only reentrancy (zustandsabhängige Reads)
    Selbst wenn du nach dem Call keinen State mehr schreibst, kann ein Angreifer eine temporäre Inkonsistenz ausnutzen, um z. B. zu beeinflussen:

  4. price-per-share-Berechnungen
  5. Collateral-Checks
  6. oracle-abhängige Logik
  7. „isHealthy“-Validierungen

  8. Token-hook/callback reentrancy

  9. ERC777 tokensReceived
  10. ERC721 onERC721Received
  11. ERC1155 onERC1155Received
  12. Custom Token Hooks in upgradeable/proxy tokens

Real-World-Impact: Reentrancy-Incidents, die du kennen solltest

  • The DAO (2016): entzog via Reentrancy in einer Split-Funktion ~3.6M ETH; löste den Hard Fork von Ethereum aus.
  • Cream Finance (Oct 2021): erlitt einen großen Exploit (gemeldet ~$130M) mit komplexen DeFi-Mechaniken; auch wenn nicht „pure DAO-style“, zeigt es, wie Composability und externe Interaktionen Risiken verstärken.
  • Verschiedene NFT-Marketplace-/Staking-Incidents (2021–2023): Reentrancy über ERC721-Receiver-Hooks und Payout-Logik führte wiederholt zu Double-Withdrawals und fehlerhaftem Accounting, wenn Callbacks nicht berücksichtigt wurden.

(Reentrancy wird zudem häufig mit flash loans kombiniert: Angreifer leihen Kapital, manipulieren State, re-entern kritische Funktionen und zahlen innerhalb einer Transaktion zurück – so wird aus einem „braucht Kapital“-Exploit ein kapitalfreier Exploit.)


Reentrancy prevention in Solidity: Muster, die wirklich funktionieren

Effektive Reentrancy prevention in Solidity läuft im Kern auf drei Dinge hinaus: (1) External Calls minimieren, (2) Logik korrekt anordnen und (3) explizite Reentrancy-Locks auf sensiblen Pfaden erzwingen.

Quotable (40–60 words):
Die zuverlässigste Reentrancy-Prevention kombiniert drei Ebenen: Checks-Effects-Interactions, damit State Updates vor External Calls passieren; Pull Payments statt Push Payouts; und Schutz kritischer Funktionen durch einen Reentrancy Guard (Mutex). Behandle außerdem Token-Callbacks und „safe“ Transfers wie External Calls mit Re-Entry-Risiko.

1) Checks-Effects-Interactions (CEI)

Behebe das vorige Beispiel, indem du den State vor dem ETH-Senden aktualisierst:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract CEIBank {
    mapping(address => uint256) public balance;

    function deposit() external payable {
        balance[msg.sender] += msg.value;
    }

    function withdraw(uint256 amount) external {
        require(balance[msg.sender] >= amount, "insufficient");

        // EFFECTS first
        balance[msg.sender] -= amount;

        // INTERACTION last
        (bool ok, ) = msg.sender.call{value: amount}("");
        require(ok, "send failed");
    }
}

Caveat: CEI ist notwendig, aber nicht immer ausreichend bei Cross-Function-Reentrancy, wenn andere Funktionen re-entert werden können und auf gemeinsamen State zugreifen.

2) Nutze einen Reentrancy Guard (Mutex)

OpenZeppelin ReentrancyGuard ist eine Standard-Mitigation, besonders für Withdrawal/Claim/Borrow-Pfade.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract GuardedBank is ReentrancyGuard {
    mapping(address => uint256) public balance;

    function deposit() external payable {
        balance[msg.sender] += msg.value;
    }

    function withdraw(uint256 amount) external nonReentrant {
        require(balance[msg.sender] >= amount, "insufficient");
        balance[msg.sender] -= amount;

        (bool ok, ) = msg.sender.call{value: amount}("");
        require(ok, "send failed");
    }
}

Wichtig: Setze nonReentrant auf alle Entry Points, die denselben sensiblen State anfassen (z. B. withdraw, claim, exit, liquidate), oder strukturiere interne Funktionen sorgfältig, um Guard-Konflikte zu vermeiden.

3) Pull Payments statt Push Payments bevorzugen

Statt in einer komplexen Funktion direkt „auszuzahlen“, kannst du einen User gutschreiben und ihn später separat withdrawen lassen.

  • Pros: weniger External Calls in der Business-Logik; kleinere Attack Surface
  • Cons: zusätzlicher UX-Schritt; erfordert sauberes Accounting und einen geschützten Withdraw

4) „Token Transfers“ im Threat Model als External Calls behandeln

Auch wenn ERC20 meistens sicher ist, ist es deine Integration möglicherweise nicht: - Transfer von ERC777-Tokens - Aufruf eines Vaults, der Shares ausgibt - Aufruf eines Routers/Aggregators - Nutzung von safeTransferFrom für NFTs (Callbacks!)

5) Vermeide beliebige External Calls in privilegierten Flows

Wenn Governance oder Admins Target-Contracts/Callbacks konfigurieren können, entstehen leicht Reentrancy-Pfade, die Annahmen aushebeln. Das wird noch gefährlicher in Kombination mit delegatecall risks in Solidity (gemeinsamer Storage-Kontext).


Vergleich: CEI vs ReentrancyGuard vs Pull Payments (was wann nutzen)

Der beste Ansatz hängt vom Zweck der Funktion, Composability-Anforderungen und UX-Constraints ab. Die meisten Production-Protokolle nutzen mehrere Layer.

Quotable (40–60 words):
Keine einzelne Mitigation passt für jedes Protokoll. Checks-Effects-Interactions verhindert die häufigsten „send then update“-Fehler, aber Cross-Function-Reentrancy kann dennoch bestehen. Reentrancy Guards fügen einen harten Mutex für sensitive Pfade hinzu. Pull Payments reduzieren External Calls in der Kernlogik, tauschen dafür UX ein und brauchen robustes Withdrawal-Accounting.

Mitigation Was sie macht Stärken Schwächen Am besten für
Checks-Effects-Interactions (CEI) Aktualisiert State vor External Calls Einfach, geringer Overhead, guter Baseline-Schutz Stoppt nicht immer Cross-Function- oder Read-only-Reentrancy Die meisten state-changing Funktionen mit External Calls
nonReentrant guard (mutex) Blockiert Re-Entry während der Ausführung Stark, explizit, weit verbreitet Kann interne Funktionsaufrufe erschweren; muss alle relevanten Entry Points abdecken Withdrawals, Claims, Borrows, Liquidations
Pull payments Schreibt Balances gut; User withdrawen später Minimiert External Calls in der Kernlogik Extra Transaktion für User; benötigt weiterhin guarded withdraw Rewards, Fee-Distributions, Refunds
Restricted external calls Limitiert, welche Contracts aufgerufen werden dürfen Reduziert unerwartete Callbacks Weniger Composability; Governance-Overhead Admin-Tools, Upgrade-Hooks, Strategy-Execution
Careful token/NFT handling Behandelt Hooks als Reentry-Vektoren Verhindert Callback-Überraschungen Erfordert tiefes Integrationswissen NFT-Marktplätze, Staking mit ERC777/4626

Audit checklist: wie man Reentrancy in echten Protokollen findet und testet

Reentrancy verhindert man per Design, aber man beweist es durch Tests, adversarial Reviews und gezielte Audits über alle External-Interaction-Pfade hinweg.

Quotable (40–60 words):
Um Reentrancy zu erkennen, identifiziere jeden External Call (ETH-Sends, Token-Transfers, Router/Vault-Interaktionen, NFT Safe Transfers) und prüfe, ob State Updates und Invariants vor dem Call finalisiert sind. Teste mit bösartigen Receiver-Contracts und Fuzzing. Achte besonders auf Cross-Function Shared State und Callback-fähige Tokens.

Praktische Checkliste (engineer-friendly)

  • Alle External Calls mappen
  • call, transfer, send
  • Token-transfer/transferFrom
  • safeTransferFrom (ERC721/1155)
  • Vault/Router/Bridge-Interaktionen
  • Gemeinsamen State finden, den mehrere Funktionen anfassen
  • balances, shares, debt, reward indices, snapshots
  • Reihenfolge prüfen
  • Sind kritische State Updates vor dem External Call abgeschlossen?
  • Invariants ergänzen
  • total assets == sum of balances (oder begrenzt)
  • shares * price-per-share Konsistenz
  • reward debt Monotonie
  • Angreifer simulieren
  • bösartiger Receiver re-entert
  • bösartiger Token mit Hooks
  • flash-loan-finanzierter Angriff, der den Impact verstärkt

Beispiel: Cross-Function-Reentrancy-Falle (Pattern)

Wenn withdraw() nach außen aufruft und claimRewards() re-entert werden kann und einen „post-withdraw“-State annimmt, kann es zu Folgendem kommen: - doppelte Reward-Claims - umgangene Cooldowns - kaputtes Reward-Debt-Accounting

Eine häufige Mitigation ist: - Accounting-Updates in einer internen Funktion zentralisieren - nonReentrant konsistent auf alle state-mutating Entry Points anwenden - externe Interaktionen ans Ende der Ausführung isolieren

Wie Soken typischerweise Reentrancy reviewed (was zu erwarten ist)

In Soken Audits machen wir gezielt: - Aufbau eines external call graph und Markierung von „reentry windows“ - Verifikation von CEI-Compliance und Mutex-Coverage - Tests von Integrationen gegen Callback-Standards (ERC777, ERC721/1155 Safe Transfers, ERC4626 Vaults) - Versuch von Exploit-PoCs in Foundry, um Severity zu validieren und Fixes zu bestätigen

Das ist besonders wichtig bei DeFi-Designs, die flash-loan-attacks-defi ausgesetzt sind, wo Angreifer Positionsgrößen skalieren und Edge Cases in einer einzigen atomaren Transaktion stressen können.


Fazit: Reentrancy ist vermeidbar – wenn du für Composability entwickelst

Reentrancy bleibt ein Top-Risiko für Smart Contracts, weil DeFi von Haus aus composable ist: Der „externe Contract“, den du aufrufst, kann ein User sein, ein Token mit Hooks, ein Vault, ein Router oder ein angreiferkontrollierter Callback. Die Lösung ist selten ein einzelner Trick – es ist diszipliniertes Engineering: CEI-Order, guarded Entry Points, External Calls minimieren und adversarial Testing.

Wenn du Staking, Lending, Governance, Bridges oder irgendeinen Contract shipst, der Assets bewegt, solltest du Reentrancy von Tag eins als Design-Constraint behandeln – nicht als Lint-Warnung am Ende.

Soken (soken.io) hilft Teams, Production-Systeme zu härten – mit smart contract auditing & penetration testing und DeFi security reviews für Vaults, Staking, Governance und komplexe Integrationen. Wenn du ein Reentrancy-fokussiertes Review willst (inklusive PoCs und Foundry-Tests), kontaktiere uns unter soken.io, um ein Audit zu planen und mit mehr Sicherheit zu shippen.

Frequently Asked Questions

Was ist ein Reentrancy-Angriff in Smart Contracts?

Ein Reentrancy-Angriff entsteht, wenn ein Contract einen externen Call ausführt, bevor er seinen eigenen Zustand aktualisiert. Ein Angreifer-Contract kann dann die verwundbare Funktion erneut betreten und wiederholen. So lassen sich Auszahlungs- oder Claim-Logiken ausnutzen und Gelder abziehen, weil Salden innerhalb derselben Transaktion inkonsistent sind.

Wie erkenne ich eine Reentrancy-Schwachstelle in Solidity-Code?

Achte auf externe Calls (ETH senden, Token-Contracts, AMMs, Vaults aufrufen), die vor kritischen State-Updates wie Balance-Abzug oder Nonce-Erhöhung stattfinden. Markiere außerdem Callbacks wie ERC777-Hooks sowie fallback/receive-Funktionen. Wenn eine Funktion während der Ausführung erneut betreten werden kann, ist sie potenziell reentrant.

Was sind Best Practices zur Reentrancy-Prävention in Solidity?

Nutze Checks-Effects-Interactions: Eingaben validieren, internen Zustand aktualisieren, dann extern interagieren. Bevorzuge Pull Payments gegenüber Push-Transfers. Setze auf sensiblen Funktionen eine Reentrancy-Guard ein (z. B. OpenZeppelin ReentrancyGuard). Reduziere externe Calls in zentralen Accounting-Pfaden und designe Interfaces so, dass Callbacks vermieden werden.

Verhindert OpenZeppelin ReentrancyGuard Reentrancy-Angriffe vollständig?

ReentrancyGuard blockiert viele Re-Entry-Muster innerhalb desselben Contracts, ist aber keine vollständige Sicherheitsgarantie. Cross-Function-Reentrancy, Callback-Flows aus externen Protokollen und fehlerhaftes Accounting können weiterhin zu Verlusten führen. Kombiniere Guards mit korrekter State-Reihenfolge (CEI), Pull-Payment-Patterns und striktem External-Call-Design.