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:
- User ruft
withdraw()auf - Contract sendet ETH/Tokens an den User (external call)
- Contract aktualisiert
balances[msg.sender](zu spät) - 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
-
Single-function reentrancy (klassisch)
withdraw()wird wiederholt re-entert. -
Cross-function reentrancy
withdraw()ruft nach außen, der Angreifer re-entertclaimRewards()oderborrow(), die sich auf gemeinsamen, nur teilweise aktualisierten State stützen. -
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: - price-per-share-Berechnungen
- Collateral-Checks
- oracle-abhängige Logik
-
„isHealthy“-Validierungen
-
Token-hook/callback reentrancy
- ERC777
tokensReceived - ERC721
onERC721Received - ERC1155
onERC1155Received - 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.