Weiter zum Hauptinhalt
Change page

Sicherheit von Smart Contracts

Letzte Änderung: @Astronaut828(opens in a new tab), 22. Mai 2024

Smart Contracts sind äußerst flexibel und in der Lage, große Mengen an Werten und Daten zu kontrollieren, während sie eine unveränderliche Logik auf der Grundlage von auf der Blockchain bereitgestelltem Code ausführen. So ist ein lebendiges Ökosystem aus vertrauenswürdigen und dezentralisierten Applikationen entstanden, das viele Vorteile gegenüber den alten Systemen bietet. Sie bieten auch eine Chance für Angreifer, die durch die Ausnutzung von Schwachstellen in Smart Contracts Profit machen wollen.

Öffentliche Blockchains wie Ethereum erschweren das Problem der Sicherung von Smart Contracts zusätzlich. Der Code des veröffentlichten Vertrags kann in der Regel nicht geändert werden, um Sicherheitslücken zu schließen, während die aus Smart Contracts gestohlenen Vermögenswerte aufgrund der Unveränderlichkeit extrem schwer nachzuverfolgen und meist nicht wiederherzustellen sind.

Obwohl die Zahlen variieren, wird geschätzt, dass der Gesamtbetrag des gestohlenen oder verlorenen Werts aufgrund von Sicherheitsmängeln in Smart Contracts weit über 1 Milliarde US-Dollar beträgt. Dies beinhaltet hochkarätige Vorfälle, wie den DAO-Hack(opens in a new tab) (3,6 Mio. ETH gestohlen, nach heutigen Preisen über 1 Milliarde US-Dollar wert), den Parity Multi-Sig Wallet-Hack(opens in a new tab) (30 Mio. US-Dollar von Hackern verloren) und das Problem mit den eingefrorenen Parity-Wallets(opens in a new tab) (über 300 Mio. US-Dollar in ETH gesperrt).

Die oben genannten Probleme machen es für Entwickler zwingend erforderlich, in die Entwicklung sicherer, robuster und widerstandsfähiger Smart Contracts zu investieren. Die Sicherheit von Smart Contracts ist eine ernste Angelegenheit, die jeder Entwickler lernen sollte. In diesem Ratgeber werden Sicherheitsüberlegungen für Ethereum-Entwickler behandelt und Ressourcen zur Verbesserung der Smart Contract-Sicherheit vorgestellt.

Voraussetzungen

Stellen Sie sicher, dass Sie mit den Grundlagen der Smart Contract-Entwicklung vertraut sind, bevor Sie sich mit der Sicherheit befassen.

Richtlinien für die Erstellung sicherer Ethereum Smart Contracts

1. Gestaltung geeigneter Zugriffskontrollen

Bei Smart Contracts können Funktionen, die als public oder external markiert sind, von beliebigen extern verwalteten Konten (EOAs) oder Vertragskonten aufgerufen werden. Die Festlegung der öffentlichen Sichtbarkeit von Funktionen ist notwendig, wenn Sie möchten, dass andere Personen mit Ihrem Vertrag interagieren können. Funktionen, die als private gekennzeichnet sind, können jedoch nur von Funktionen innerhalb des Smart Contracts aufgerufen werden, nicht von externen Accounts. Jedem Netzwerkteilnehmer Zugang zu Vertragsfunktionen zu gewähren, kann zu Problemen führen, insbesondere wenn dies bedeutet, dass jeder Nutzer sensible Operationen durchführen kann (z. B. das Minting neuer Token).

Um die unbefugte Nutzung von Smart Contract-Funktionen zu verhindern, müssen sichere Zugriffskontrollen implementiert werden. Die Zugriffskontrolle beschränkt die Möglichkeit, bestimmte Funktionen in einem Smart Contract zu nutzen, auf zugelassene Stellen, wie z. B. die für die Verwaltung des Vertrags zuständigen Konten. Das Ownable-Modell und Rollenbasierte Kontrolle sind zwei Parameter, die für die Implementierung der Zugriffskontrolle in Smart Contracts nützlich sind:

Ownable-Modell

Beim Ownable-Modell wird während der Vertragserstellung eine Adresse als „Eigentümer“ des Vertrags festgelegt. Geschützten Funktionen wird ein OnlyOwner-Modifikator zugewiesen, der sicherstellt, dass der Vertrag die Identität der aufrufenden Adresse authentifiziert, bevor die Funktion ausgeführt wird. Aufrufe geschützter Funktionen von anderen Adressen als der des Vertragseigentümers werden immer zurückgewiesen, um unerwünschte Zugriffe zu verhindern.

Rollenbasierte Zugriffskontrolle

Die Registrierung einer einzigen Adresse als Eigentümer in einem Smart Contract birgt das Risiko der Zentralisierung und stellt einen einzelnen Ausfallpunkt dar. Wenn die Kontoschlüssel des Eigentümers gefährdet sind, können Angreifer den entsprechenden Vertrag angreifen. Aus diesem Grund kann die Verwendung eines rollenbasierten Zugriffskontrollmusters mit mehreren administrativen Konten eine bessere Option sein.

Bei der rollenbasierten Zugriffskontrolle wird der Zugriff auf sensible Funktionen auf eine Reihe von vertrauenswürdigen Teilnehmern verteilt. So kann beispielsweise ein Konto für das Minting von Token zuständig sein, während ein anderes Konto Upgrades durchführt oder den Vertrag pausiert. Durch diese dezentrale Zugriffskontrolle werden „einzelne Ausfallpunkte“ eliminiert und die Vertrauensvoraussetzungen für Benutzer reduziert.

Verwendung von Wallets mit Multi-Signature-Option

Ein weiterer Ansatz für die Implementierung einer sicheren Zugriffskontrolle ist die Verwendung eines Multi-Signatur-Kontos zur Verwaltung eines Vertrags. Im Gegensatz zu einem regulären EOA sind Multi-Signatur-Konten das Eigentum von mehreren Instanzen und erfordern Signaturen von einer Mindestanzahl von Konten, beispielsweise 3 von 5, um Transaktionen auszuführen.

Die Verwendung einer Mehrfachsignatur für die Zugriffskontrolle führt eine zusätzliche Sicherheitsebene ein, da Aktionen auf dem Zielvertrag die Zustimmung von mehreren Parteien erfordern. Dies ist besonders nützlich, wenn die Verwendung der Ownable-Funktion erforderlich ist, da es für einen Angreifer oder einen böswilligen Insider schwieriger ist, sensible Vertragsfunktionen für böswillige Zwecke zu manipulieren.

2. Verwenden Sie die Anweisungen require(), assert() und revert(), um Vertragsoperationen zu schützen.

Wie bereits erwähnt, kann jeder Nutzer öffentliche Funktionen in Ihrem Smart Contract aufrufen, sobald dieser auf der Blockchain veröffentlicht wurde. Da Sie nicht im Voraus wissen können, wie externe Konten mit einem Vertrag interagieren werden, ist es ideal, interne Schutzmaßnahmen gegen problematische Funktionen zu implementieren, bevor Sie sie Veröffentlichen. Sie können korrektes Verhalten in Smart Contracts durch die Verwendung der Anweisungen require(), assert() und revert() erzwingen, um Ausnahmen auszulösen und Zustandsänderungen rückgängig zu machen, wenn die Ausführung bestimmte Anforderungen nicht erfüllt.

require(): require werden am Anfang von Funktionen definiert und stellen sicher, dass vordefinierte Bedingungen erfüllt sind, bevor die aufgerufene Funktion ausgeführt wird. Eine require-Anweisung kann verwendet werden, um Nutzereingaben zu validieren, Zustandsvariablen zu überprüfen oder die Identität des aufrufenden Accounts zu authentifizieren, bevor eine Funktion ausgeführt wird.

assert(): assert() wird verwendet, um interne Fehler zu erkennen und Verletzungen von „Invarianten“ in Ihrem Code zu überprüfen. Eine Invariante ist eine logische Behauptung über den Zustand eines Vertrags, die für alle Funktionsausführungen gelten soll. Ein Beispiel für eine Invariante ist das maximale Gesamtangebot oder der maximale Saldo eines Token-Vertrags. Die Verwendung von assert() stellt sicher, dass Ihr Vertrag niemals einen verwundbaren Zustand erreicht, und falls doch, werden alle Änderungen an den Zustandsvariablen zurückgesetzt.

revert(): revert() kann in einer if-else-Anweisung verwendet werden, die eine Ausnahme auslöst, wenn die erforderliche Bedingung nicht erfüllt ist. Der folgende Mustervertrag verwendet revert(), um die Ausführung von Funktionen zu überwachen:

1pragma solidity ^0.8.4;
2
3contract VendingMachine {
4 address owner;
5 error Unauthorized();
6 function buy(uint amount) public payable {
7 if (amount > msg.value / 2 ether)
8 revert("Not enough Ether provided.");
9 // Perform the purchase.
10 }
11 function withdraw() public {
12 if (msg.sender != owner)
13 revert Unauthorized();
14
15 payable(msg.sender).transfer(address(this).balance);
16 }
17}
Alles anzeigen

3. Testen Sie Smart Contracts und überprüfen Sie die Richtigkeit des Codes

Die Unveränderlichkeit des Codes, der auf der Ethereum Virtual Machine läuft, bedeutet, dass Smart Contracts ein höheres Maß an Qualitätsbewertung während der Entwicklungsphase erfordern. Wenn Sie Ihren Vertrag ausgiebig testen und auf unerwartete Ergebnisse achten, verbessern Sie die Sicherheit erheblich und schützen Ihre Nutzer auf lange Sicht.

Die übliche Methode besteht darin, kleine Unit-Tests mit Scheindaten zu schreiben, die der Vertrag von den Nutzern erhalten würde. Unit-Testing ist gut, um die Funktionalität bestimmter Funktionen zu testen und sicherzustellen, dass ein Smart Contract wie erwartet funktioniert.

Leider sind Unit-Tests für die Verbesserung der Sicherheit von Smart Contracts nur wenig effektiv, wenn sie nur isoliert angewendet werden. Ein Unit-Test kann beweisen, dass eine Funktion bei Mock-Daten korrekt ausgeführt wird, Unit-Tests sind jedoch nur so effektiv wie die Tests, die verfasst werden. Das macht es schwierig, unentdeckte Sonderfälle und Schwachstellen zu erkennen, die die Sicherheit Ihres Smart Contracts gefährden könnten.

Ein besserer Ansatz besteht darin, Unit-Tests mit eigenschaftsbasierten Tests zu kombinieren, die mit statischer und dynamischer Analyse durchgeführt werden. Die statische Analyse stützt sich auf Low-Level-Darstellungen, wie Kontrollflussdiagramme(opens in a new tab) und abstrakte Syntaxstrukturen(opens in a new tab), um die erreichbaren Programmzustände und Ausführungspfade zu analysieren. Bei dynamischen Analysetechniken wie Fuzzing wird Vertragscode mit zufälligen Eingabewerten ausgeführt, um Operationen zu erkennen, die gegen Sicherheitseigenschaften verstoßen.

Die formale Verifizierung ist eine weitere Technik zur Überprüfung der Sicherheitseigenschaften von Smart Contracts. Im Gegensatz zu regulären Tests kann die formale Verifizierung schlüssig beweisen, dass ein Smart Contract keine Fehler enthält. Dies wird erreicht, indem eine formale Spezifikation erstellt wird, die die gewünschten Sicherheitseigenschaften festhält, um dann zu gewährleisten, dass ein Formmodell des Vertrags mit dieser Spezifikation übereinstimmt.

4. Bitten Sie um eine unabhängige Überprüfung Ihres Codes

Nachdem Sie Ihren Vertrag getestet haben, sollten Sie andere bitten, den Quellcode auf Sicherheitsprobleme zu prüfen. Beim Testen werden nicht alle Schwachstellen in einem Smart Contract aufgedeckt, eine unabhängige Überprüfung erhöht jedoch die Wahrscheinlichkeit, dass Schwachstellen entdeckt werden.

Audits (Prüfungen)

Die Beauftragung eines Smart Contract-Audits ist eine Möglichkeit zur Durchführung einer unabhängigen Code-Überprüfung. Prüfer spielen eine wichtige Rolle, wenn es darum geht sicherzustellen, dass Smart Contracts sicher und frei von Qualitätsmängeln und Planungsfehlern sind.

Dennoch sollten Sie Audits nicht als Wunderwaffe betrachten. Smart Contract-Audits können nicht jeden Fehler aufspüren und sind hauptsächlich dazu gedacht, eine zusätzliche Runde von Überprüfungen durchzuführen, die dazu beitragen können, Probleme zu entdecken, die von den Entwicklern während der anfänglichen Entwicklung und Tests übersehen wurden. Sie sollten auch die Best Practices für die Zusammenarbeit mit Prüfern(opens in a new tab) befolgen, z. B. den Code ordnungsgemäß dokumentieren und Inline-Kommentare hinzufügen, um den Nutzen eines Smart Contract-Audits zu maximieren.

Aufdecken von Fehlern

Die Einrichtung eines Prämienprogramms für das Aufdecken von Fehlern (Bug Bounty Program) ist ein weiterer Ansatz zur Durchführung externer Codeüberprüfungen. Ein Bug Bounty ist eine finanzielle Belohnung für Personen (in der Regel Whitehat-Hacker), die Schwachstellen in einer Applikation entdecken.

Wenn sie richtig eingesetzt werden, geben Bug Bounties den Mitgliedern der Hacker-Community einen Anreiz, Ihren Code auf kritische Fehler zu untersuchen. Ein reales Beispiel ist der „Infinite Money Bug“, der es einem Angreifer ermöglicht hätte, eine unbegrenzte Menge an Ether auf Optimism(opens in a new tab), einem Layer 2-Protokoll, das auf Ethereum läuft, zu erzeugen. Glücklicherweise entdeckte ein Whitehat-Hacker den Fehler(opens in a new tab) und meldete ihn dem Team, und erhielt dafür eine hohe Belohnung(opens in a new tab).

Eine sinnvolle Strategie besteht darin, die Auszahlung eines Bug-Bounty-Programms im Verhältnis zur Höhe der auf dem Spiel stehenden Mittel festzulegen. Dieser als „Skalierung zum Aufdecken von Fehlern(opens in a new tab)“ bezeichnete Ansatz bietet finanzielle Anreize für Einzelpersonen, Schwachstellen verantwortungsbewusst offenzulegen, anstatt sie auszunutzen.

5. Befolgen Sie bei der Entwicklung von Smart Contracts die bewährten Methoden

Die Verfügbarkeit von Audits und Bug Bounties entbindet Sie nicht von Ihrer Verantwortung, qualitativ hochwertigen Code zu schreiben. Die Sicherheit von Smart Contracts beginnt mit der Einhaltung geeigneter Planungs- und Entwicklungsprozesse:

  • Speichern Sie den gesamten Code in einem Versionskontrollsystem, z. B. Git

  • Nehmen Sie alle Codeänderungen über Pull Requests vor

  • Stellen Sie sicher, dass Pull-Requests mindestens einen unabhängigen Reviewer haben. Wenn Sie alleine an einem Projekt arbeiten, sollten Sie überlegen, ob Sie nicht andere Entwickler finden und mit diesen Code-Reviews austauschen

  • Verwendung einer Entwicklungsumgebung zum Testen, Kompilieren und Bereitstellen von Smart Contracts

  • Überprüfen Sie Ihren Code mit grundlegenden Code-Analysetools wie Mythril und Slither. Idealerweise sollten Sie dies tun, noch bevor eine Pull-Anfrage eingebunden wird, und die Unterschiede in der Ergebnisausgabe vergleichen

  • Stellen Sie sicher, dass Ihr Code ohne Fehler kompiliert wird und der Solidity-Compiler keine Warnungen ausgibt

  • Dokumentieren Sie Ihren Code ordnungsgemäß (unter Verwendung von NatSpec(opens in a new tab)) und beschreiben Sie Einzelheiten der Vertragsarchitektur in leicht verständlicher Sprache. Dies erleichtert es anderen, Ihren Code zu überprüfen und zu kontrollieren.

6. Umsetzung solider Pläne für die Notfallwiederherstellung

Die Entwicklung sicherer Zugriffskontrollen, die Implementierung von Funktionsmodifikatoren und andere Vorschläge können die Sicherheit von Smart Contracts verbessern, jedoch können sie die Möglichkeit böswilliger Angriffe nicht ausschließen. Der Aufbau sicherer Smart Contracts erfordert eine „Vorbereitung auf Fehler“ und einen Notfallplan, um wirksam auf Angriffe reagieren zu können. Ein angemessener Notfallwiederherstellungsplan umfasst einige oder alle der folgenden Komponenten:

Aktualisierungen von Verträgen

Obwohl Ethereum Smart Contracts standardmäßig unveränderlich sind, ist es möglich, durch die Verwendung von Upgrade-Mustern einen gewissen Grad an Veränderbarkeit zu erreichen. Die Aktualisierung von Verträgen ist dann erforderlich, wenn ein kritischer Fehler Ihren alten Vertrag unbrauchbar macht und die Einführung einer neuen Logik die sinnvollste Option darstellt.

Die Mechanismen zur Aktualisierung von Verträgen funktionieren unterschiedlich, wobei jedoch das „Proxy-Muster“ einer der beliebtesten Ansätze für die Aktualisierung von Smart Contracts ist. Proxy-Muster teilen den Zustand und die Logik einer Anwendung zwischen zwei Verträgen auf. Der erste Vertrag (ein so genannter „Proxy-Vertrag“) speichert Zustandsvariablen (z. B. Benutzerguthaben), während der zweite Vertrag (ein so genannter „Logik-Vertrag“) den Code für die Ausführung von Vertragsfunktionen enthält.

Konten interagieren mit dem Proxy-Vertrag, der alle Funktionsaufrufe über den delegatecall()(opens in a new tab) ein Low-Level-Aufruf, an den Logik-Vertrag weiterleitet. Im Gegensatz zu einem normalen Aufruf stellt delegatecall() sicher, dass der Code, welcher unter der Adresse des logischen Vertrags läuft, im Kontext des aufrufenden Vertrags ausgeführt wird. Das bedeutet, dass der Logikvertrag immer in den Speicher des Proxys schreibt (anstatt in seinen eigenen Speicher) und die ursprünglichen Werte von msg.sender und msg.value erhalten bleiben.

Die Übertragung von Aufrufen an den Logikvertrag erfordert die Speicherung seiner Adresse im Speicher des Proxy-Vertrags. Um die Logik des Vertrags zu aktualisieren, muss daher nur ein anderer Logikvertrag eingesetzt und die neue Adresse im Proxy-Vertrag gespeichert werden. Da nachfolgende Aufrufe des Proxy-Vertrags automatisch an den neuen Logik-Vertrag weitergeleitet werden, hätten Sie den Vertrag „aktualisiert“, ohne den Code tatsächlich zu ändern.

Mehr zum Thema Aktualisieren von Verträgen.

Notausschalter

Wie bereits erwähnt, können umfangreiche Prüfungen und Tests unmöglich alle Fehler in einem Smart Contract aufdecken. Wenn eine Schwachstelle in Ihrem Code nach der Veröffentlichung auftritt, ist es unmöglich, sie zu beheben, da Sie den Code, der unter der Vertragsadresse läuft, nicht ändern können. Außerdem kann die Implementierung von Upgrade-Mechanismen (z. B. Proxy-Muster) einige Zeit in Anspruch nehmen (sie erfordern oft die Zustimmung verschiedener Parteien), was den Angreifern nur mehr Zeit gibt, um mehr Schaden anzurichten.

Die einzige Möglichkeit besteht darin, eine „Not-Aus“-Funktion zu implementieren, die Aufrufe anfälliger Funktionen in einem Vertrag blockiert. Notausschalter bestehen in der Regel aus den folgenden Komponenten:

  1. Eine globale Boolesche Variable, die angibt, ob sich der Smart Contract in einem gestoppten Zustand befindet oder nicht. Diese Variable wird bei der Einrichtung des Vertrags auf false gesetzt, schaltet aber auf true um, sobald der Vertrag gestoppt wird.

  2. Funktionen, die bei ihrer Ausführung auf die boolesche Variable verweisen. Auf diese Funktionen kann zugegriffen werden, wenn der Smart Contract in Betrieb ist, und sie werden unzugänglich, wenn die Notausfunktion ausgelöst wird.

  3. Eine Person, die Zugriff auf die Notausfunktion hat, die die Boolesche Variable auf true schaltet. Um böswillige Aktionen zu verhindern, kann der Aufruf dieser Funktion auf eine vertrauenswürdige Adresse (z. B. den Vertragsinhaber) beschränkt werden.

Sobald der Vertrag den Notausschalter aktiviert, sind bestimmte Funktionen nicht mehr aufrufbar. Dies wird erreicht, indem die ausgewählten Funktionen in einen Modifikator verpackt werden, der auf die globale Variable verweist. Im Folgenden wird ein Beispiel(opens in a new tab) für die Umsetzung dieses Konzepts in einem Vertrag beschrieben:

1// This code has not been professionally audited and makes no promises about safety or correctness. Use at your own risk.
2
3contract EmergencyStop {
4
5 bool isStopped = false;
6
7 modifier stoppedInEmergency {
8 require(!isStopped);
9 _;
10 }
11
12 modifier onlyWhenStopped {
13 require(isStopped);
14 _;
15 }
16
17 modifier onlyAuthorized {
18 // Check for authorization of msg.sender here
19 _;
20 }
21
22 function stopContract() public onlyAuthorized {
23 isStopped = true;
24 }
25
26 function resumeContract() public onlyAuthorized {
27 isStopped = false;
28 }
29
30 function deposit() public payable stoppedInEmergency {
31 // Deposit logic happening here
32 }
33
34 function emergencyWithdraw() public onlyWhenStopped {
35 // Emergency withdraw happening here
36 }
37}
Alles anzeigen
Kopieren

Dieses Beispiel zeigt die grundlegenden Merkmale von Notstopps:

  • isStopped ist ein Boolescher Wert, der zu Beginn false ergibt und true, wenn der Vertrag in den Notfallmodus wechselt.

  • Die Funktionsmodifikatoren onlyWhenStopped und stoppedInEmergency überprüfen die Variable isStopped. stoppedInEmergency wird verwendet, um Funktionen zu steuern, die nicht zugänglich sein sollten, wenn der Vertrag gefährdet ist (z. B. deposit()). Aufrufe dieser Funktionen werden einfach zurückgewiesen.

onlyWhenStopped wird für Funktionen verwendet, die in einem Notfall aufrufbar sein sollten (z. B. emergencyWithdraw()). Diese Funktionen können zur Lösung des Problems beitragen, weshalb sie nicht in der Liste der „eingeschränkten Funktionen“ aufgeführt sind.

Die Verwendung einer Notstopp-Funktion ist eine wirksame Notlösung für den Umgang mit schwerwiegenden Schwachstellen in Ihrem Smart Contract. Allerdings müssen die Nutzer darauf vertrauen können, dass die Entwickler sie nicht aus eigennützigen Gründen aktivieren. Zu diesem Zweck kann die Kontrolle über den Notstopp dezentralisiert werden, indem er entweder einem On-Chain-Abstimmungsmechanismus, einer Zeitsperre oder der Genehmigung durch eine Multisig-Wallet unterworfen wird.

Überwachung von Ereignissen

Ereignisse(opens in a new tab) ermöglichen es Ihnen, Aufrufe von Smart Contract-Funktionen zu verfolgen und Änderungen an Zustandsvariablen zu überwachen. Ideal ist es, wenn Sie Ihren Smart Contract so programmieren, dass er immer dann ein Ereignis auslöst, wenn eine Partei eine sicherheitskritische Aktion durchführt (z. B. das Abheben von Guthaben).

Die Protokollierung von Ereignissen und deren Überwachung off-chain bietet Einblicke in Vertragsvorgänge und hilft, böswillige Handlungen schneller zu entdecken. Das bedeutet, dass Ihr Team schneller auf Hacks reagieren und Maßnahmen ergreifen kann, um die Auswirkungen auf die Benutzer zu minimieren, z. B. das Anhalten von Funktionen oder die Durchführung eines Upgrades.

Sie können sich auch für ein handelsübliches Überwachungsprogramm entscheiden, das automatisch Warnmeldungen weiterleitet, sobald jemand mit Ihren Verträgen interagiert. Mit diesen Tools können Sie benutzerdefinierte Warnmeldungen erstellen, die auf verschiedenen Auslösern basieren, z. B. dem Transaktionsvolumen, der Häufigkeit von Funktionsaufrufen oder den spezifischen Funktionen. Sie könnten zum Beispiel eine Warnung programmieren, die eingeht, wenn der in einer einzigen Transaktion abgehobene Betrag einen bestimmten Schwellenwert überschreitet.

7. Sichere Governance-Systeme (Verwaltungssysteme) entwerfen

Vielleicht möchten Sie Ihre Anwendung dezentralisieren, indem Sie die Kontrolle über die wichtigsten Smart Contracts an Community-Mitglieder übergeben. In diesem Fall wird das Smart Contract-System ein Governance-Modul enthalten – einen Mechanismus, der es den Community-Mitgliedern ermöglicht, administrative Maßnahmen über ein On-Chain-Governance-System zu genehmigen. So können die Token-Inhaber beispielsweise über einen Vorschlag abstimmen, einen Proxy-Vertrag auf eine neue Implementierung zu aktualisieren.

Eine dezentrale Verwaltung kann von Vorteil sein, insbesondere weil sie die Interessen von Entwicklern und Endnutzern in Einklang bringt. Dennoch können die Mechanismen zur Steuerung von Smart Contracts bei falscher Umsetzung neue Risiken mit sich bringen. Ein plausibles Szenario ist, dass ein Angreifer durch die Aufnahme eines Flash-Darlehens enorme Stimmkraft (gemessen an der Anzahl der gehaltenen Token) erlangt und einen böswilligen Vorschlag durchsetzt.

Eine Möglichkeit zur Vermeidung von Problemen im Zusammenhang mit der On-Chain-Governance besteht darin, eine Zeitsperre zu nutzen(opens in a new tab). Eine Zeitsperre verhindert, dass ein Smart Contract bestimmte Aktionen ausführt, bis eine bestimmte Zeitspanne verstrichen ist. Andere Strategien bestehen darin, jedem Token ein „Stimmgewicht“ zuzuweisen, das sich danach richtet, wie lange er gesperrt war, oder die Stimmkraft einer Adresse in einem historischen Zeitraum (z. B. 2-3 Blöcke in der Vergangenheit) anstelle des aktuellen Blocks zu messen. Beide Methoden verringern die Möglichkeit, schnell Stimmrechte anzuhäufen, um On-Chain-Abstimmungen zu beeinflussen.

Mehr über die Planung sicherer Governance-Systeme(opens in a new tab) und verschiedene Abstimmungsmechanismen in DAOs(opens in a new tab).

8. Reduzierung der Komplexität des Codes auf ein Minimum

Traditionelle Softwareentwickler sind mit dem KISS-Prinzip („Keep it simple, stupid“) vertraut, das davon abrät, unnötige Komplexität in das Softwaredesign einzubringen. Dies entspricht der seit langem vertretenen Auffassung, dass „komplexe Systeme auf komplexe Weise versagen“ und anfälliger für kostspielige Fehler sind.

Beim Schreiben von Smart Contracts ist es besonders wichtig, die Inhalte einfach zu halten, da Smart Contracts potenziell große Wertbeträge kontrollieren. Ein Tipp zur Vereinfachung beim Schreiben von intelligenten Verträgen ist die Wiederverwendung bestehender Bibliotheken, wie OpenZeppelin Contracts(opens in a new tab), wo immer möglich. Da diese Bibliotheken von den Entwicklern ausgiebig geprüft und getestet wurden, verringert sich durch ihre Verwendung die Wahrscheinlichkeit, dass durch das Schreiben neuer Funktionen von Grund auf Fehler eingeführt werden.

Ein weiterer allgemeiner Ratschlag lautet, kleine Funktionen zu schreiben und Verträge modulartig zu halten, indem die Logik auf mehrere Verträge aufgeteilt wird. Das Schreiben von einfacherem Code verringert nicht nur die Angriffsfläche in einem Smart Contract, sondern macht es auch einfacher, Rückschlüsse auf die Korrektheit des Gesamtsystems zu ziehen und mögliche Planungsfehler frühzeitig zu erkennen.

9. Schutz vor häufigen Schwachstellen in Smart Contracts

Wiederholungsangriffe

Die EVM erlaubt keine Nebenläufigkeit, was bedeutet, dass zwei Verträge, die an einem Nachrichtenaufruf beteiligt sind, nicht gleichzeitig ausgeführt werden können. Ein externer Aufruf unterbricht die Ausführung und den Speicher des aufrufenden Vertrags, bis der Aufruf erwidert wird, woraufhin die Ausführung normal fortgesetzt wird. Dieser Vorgang kann formal als Übertragung des Kontrollflusses(opens in a new tab) auf einen anderen Vertrag beschrieben werden.

Die Übertragung des Kontrollflusses an nicht vertrauenswürdige Verträge ist zwar meist harmlos, kann aber Probleme verursachen, wie z. B. Wiederholungsangriffe. Ein Wiederholungsangriff liegt vor, wenn ein böswilliger Vertrag in einen gefährdeten Vertrag eingreift, bevor der ursprüngliche Funktionsaufruf abgeschlossen ist. Diese Art des Angriffs lässt sich am besten anhand eines Beispiels erklären.

Nehmen wir einen einfachen Smart Contract („Opfer“), der es jedem erlaubt, Ether einzuzahlen und abzuheben:

1// This contract is vulnerable. Do not use in production
2
3contract Victim {
4 mapping (address => uint256) public balances;
5
6 function deposit() external payable {
7 balances[msg.sender] += msg.value;
8 }
9
10 function withdraw() external {
11 uint256 amount = balances[msg.sender];
12 (bool success, ) = msg.sender.call.value(amount)("");
13 require(success);
14 balances[msg.sender] = 0;
15 }
16}
Alles anzeigen
Kopieren

Dieser Vertrag stellt eine withdraw()-Funktion zur Verfügung, die es den Nutzern ermöglicht, zuvor in den Vertrag eingezahlte ETH abzuheben. Bei der Bearbeitung einer Abhebung führt der Vertrag die folgenden Vorgänge durch:

  1. Überprüft das ETH-Guthaben des Nutzers
  2. Sendet Guthaben an die anrufende Adresse
  3. Setzt das Guthaben auf 0 zurück und verhindert so weitere Abhebungen durch den Nutzer

Die Funktion withdraw() im Victim-Vertrag folgt einem „Prüfungen-Auswirkungen-Interaktionen“-Modell. Sie prüft, ob die für die Ausführung notwendigen Bedingungen erfüllt sind (d. h. der Nutzer hat ein positives ETH-Guthaben) und führt die Interaktion durch, indem sie ETH an die Adresse des Aufrufers sendet, bevor sie die Auswirkungen der Transaktion anwendet (d. h. das Guthaben des Nutzers reduziert).

Wenn withdraw() von einem extern betriebenen Konto (EOA) aufgerufen wird, wird die Funktion wie erwartet ausgeführt: msg.sender.call.value() sendet ETH an den Aufrufer. Wenn msg.sender jedoch ein Smart Contract-Konto ist, welches withdraw() aufruft, wird das Senden von Geldmitteln mit msg.sender.call.value() auch die Ausführung von Code auslösen, der unter dieser Adresse gespeichert ist.

Stellen Sie sich vor, dass dies der Code ist, der an der Vertragsadresse veröffentlicht wird:

1 contract Attacker {
2 function beginAttack() external payable {
3 Victim(victim_address).deposit.value(1 ether)();
4 Victim(victim_address).withdraw();
5 }
6
7 function() external payable {
8 if (gasleft() > 40000) {
9 Victim(victim_address).withdraw();
10 }
11 }
12}
Alles anzeigen
Kopieren

Dieser Vertrag ist darauf ausgelegt, drei Dinge zu tun:

  1. Eine Einzahlung von einem anderen Konto akzeptieren (wahrscheinlich von der EOA des Angreifers)
  2. 1 ETH in den Vertrag des Opfers einzahlen
  3. Die im Smart Contract gespeicherten 1 ETH abheben

Hier ist nichts verkehrt, außer dass Attacker eine weitere Funktion hat, die withdraw() im Victim erneut aufruft, wenn das Gas, das vom eingehenden msg.sender.call.value übrig bleibt, mehr als 40.000 beträgt. Dies gibt Attacker die Möglichkeit, Victim erneut zu betreten und mehr Geld abzuheben, bevor der erste Aufruf von withdraw abgeschlossen ist. Der Kreislauf sieht folgendermaßen aus:

1- Attacker's EOA calls `Attacker.beginAttack()` with 1 ETH
2- `Attacker.beginAttack()` deposits 1 ETH into `Victim`
3- `Attacker` calls `withdraw() in `Victim`
4- `Victim` checks `Attacker`’s balance (1 ETH)
5- `Victim` sends 1 ETH to `Attacker` (which triggers the default function)
6- `Attacker` calls `Victim.withdraw()` again (note that `Victim` hasn’t reduced `Attacker`’s balance from the first withdrawal)
7- `Victim` checks `Attacker`’s balance (which is still 1 ETH because it hasn’t applied the effects of the first call)
8- `Victim` sends 1 ETH to `Attacker` (which triggers the default function and allows `Attacker` to reenter the `withdraw` function)
9- The process repeats until `Attacker` runs out of gas, at which point `msg.sender.call.value` returns without triggering additional withdrawals
10- `Victim` finally applies the results of the first transaction (and subsequent ones) to its state, so `Attacker`’s balance is set to 0
Alles anzeigen
Kopieren

Da das Guthaben des Aufrufers nicht auf 0 gesetzt wird, bevor die Funktion ausgeführt wurde, können nachfolgende Aufrufe erfolgreich sein und dem Aufrufer ermöglichen, sein Guthaben mehrmals abzuheben. Diese Art von Angriff kann genutzt werden, um einem Smart Contract das Kapital zu entziehen, wie es beim 2016 DAO-Hack(opens in a new tab) geschehen ist. Wiederholungsangriffe sind auch heute noch ein kritisches Thema für Smart Contracts, wie öffentliche Auflistungen von Reentrancy-Exploits(opens in a new tab) zeigen.

So verhindert man Wiederholungsangriffe

Ein Ansatz für den Umgang mit Wiederholungsangriffen ist das Prüfungen-Auswirkungen-Interaktionen-Modell(opens in a new tab). Dieses Modell ordnet die Ausführung von Funktionen so an, dass Code, der vor der Ausführung notwendige Überprüfungen durchführt, zuerst kommt, gefolgt von Code, der den Vertragsstatus manipuliert, und Code, der mit anderen Verträgen oder EOAs interagiert, als letztes erfolgt.

Das „Prüfungen-Auswirkungen-Interaktionen“-Modell wird in einer überarbeiteten Version des Victim-Vertrags verwendet, wie unten gezeigt:

1contract NoLongerAVictim {
2 function withdraw() external {
3 uint256 amount = balances[msg.sender];
4 balances[msg.sender] = 0;
5 (bool success, ) = msg.sender.call.value(amount)("");
6 require(success);
7 }
8}
Kopieren

Dieser Vertrag führt eine Überprüfung des Guthabens des Nutzers durch, wendet die Auswirkungen der withdraw()-Funktion an (indem das Guthaben des Nutzers auf 0 zurückgesetzt wird) und fährt mit der Interaktion (Senden von ETH an die Adresse des Nutzers) fort. Auf diese Weise wird sichergestellt, dass der Vertrag seinen Speicher vor dem externen Aufruf aktualisiert und die Bedingung der Wiederverknüpfung, die den ersten Angriff ermöglichte, beseitigt. Der Attacker-Vertrag könnte immer noch zurück in NoLongerAVictim aufrufen, aber da balances[msg.sender] auf 0 gesetzt wurde, werden zusätzliche Abhebungen einen Fehler auslösen.

Eine andere Möglichkeit ist die Verwendung einer gegenseitigen Ausschlusssperre (allgemein als „Mutex“ bezeichnet), die einen Teil des Vertragsstatus sperrt, bis ein Funktionsaufruf abgeschlossen ist. Dies wird durch eine Boolesche Variable realisiert, die vor der Ausführung der Funktion auf true gesetzt wird und nach Beendigung des Aufrufs wieder auf false zurückkehrt. Wie im folgenden Beispiel zu sehen ist, schützt die Verwendung einer „Mutex“ eine Funktion vor wiederholten Aufrufen, während der ursprüngliche Aufruf noch in Bearbeitung ist, wodurch Wiederholungsangriffe effektiv verhindert werden.

1pragma solidity ^0.7.0;
2
3contract MutexPattern {
4 bool locked = false;
5 mapping(address => uint256) public balances;
6
7 modifier noReentrancy() {
8 require(!locked, "Blocked from reentrancy.");
9 locked = true;
10 _;
11 locked = false;
12 }
13 // This function is protected by a mutex, so reentrant calls from within `msg.sender.call` cannot call `withdraw` again.
14 // The `return` statement evaluates to `true` but still evaluates the `locked = false` statement in the modifier
15 function withdraw(uint _amount) public payable noReentrancy returns(bool) {
16 require(balances[msg.sender] >= _amount, "No balance to withdraw.");
17
18 balances[msg.sender] -= _amount;
19 bool (success, ) = msg.sender.call{value: _amount}("");
20 require(success);
21
22 return true;
23 }
24}
Alles anzeigen
Kopieren

Sie können auch ein Pull-Zahlungssystem(opens in a new tab) verwenden, bei dem die Nutzer Geld von den Smart Contracts abheben müssen, anstelle eines „Push-Zahlungssystems“, das Guthaben an Konten sendet. Dies verhindert die Möglichkeit, unbeabsichtigt Code an unbekannten Adressen auszulösen (und kann auch bestimmte Denial-of-Service-Angriffe verhindern).

Integer-Unterläufe und -Überläufe

Ein Integer-Überlauf tritt auf, wenn das Ergebnis einer arithmetischen Operation außerhalb des zulässigen Wertebereichs liegt, so dass es auf den niedrigsten darstellbaren Wert „überläuft“. Zum Beispiel kann ein uint8 nur Werte bis zu 2^8-1=255 speichern. Arithmetische Operationen, die zu höheren Werten als 255 führen, führen zu einem Überlauf und setzen uint auf 0 zurück, ähnlich wie der Kilometerzähler eines Autos auf 0 zurückgesetzt wird, wenn er den maximalen Kilometerstand (999999) erreicht hat.

Integer-Unterläufe treten aus ähnlichen Gründen auf: Die Ergebnisse einer arithmetischen Operation liegen unterhalb des zulässigen Bereichs. Angenommen, Sie versuchen, 0 in einem uint8 zu dekrementieren, würde das Ergebnis einfach auf den maximal darstellbaren Wert (255) übergehen.

Sowohl Integer-Überläufe als auch -Unterläufe können zu unerwarteten Änderungen an den Zustandsvariablen eines Vertrags führen und eine ungeplante Ausführung zur Folge haben. Das folgende Beispiel zeigt, wie ein Angreifer einen arithmetischen Überlauf in einem Smart Contract ausnutzen kann, um eine ungültige Operation durchzuführen:

1pragma solidity ^0.7.6;
2
3// This contract is designed to act as a time vault.
4// User can deposit into this contract but cannot withdraw for at least a week.
5// User can also extend the wait time beyond the 1 week waiting period.
6
7/*
81. Deploy TimeLock
92. Deploy Attack with address of TimeLock
103. Call Attack.attack sending 1 ether. You will immediately be able to
11 withdraw your ether.
12
13What happened?
14Attack caused the TimeLock.lockTime to overflow and was able to withdraw
15before the 1 week waiting period.
16*/
17
18contract TimeLock {
19 mapping(address => uint) public balances;
20 mapping(address => uint) public lockTime;
21
22 function deposit() external payable {
23 balances[msg.sender] += msg.value;
24 lockTime[msg.sender] = block.timestamp + 1 weeks;
25 }
26
27 function increaseLockTime(uint _secondsToIncrease) public {
28 lockTime[msg.sender] += _secondsToIncrease;
29 }
30
31 function withdraw() public {
32 require(balances[msg.sender] > 0, "Insufficient funds");
33 require(block.timestamp > lockTime[msg.sender], "Lock time not expired");
34
35 uint amount = balances[msg.sender];
36 balances[msg.sender] = 0;
37
38 (bool sent, ) = msg.sender.call{value: amount}("");
39 require(sent, "Failed to send Ether");
40 }
41}
42
43contract Attack {
44 TimeLock timeLock;
45
46 constructor(TimeLock _timeLock) {
47 timeLock = TimeLock(_timeLock);
48 }
49
50 fallback() external payable {}
51
52 function attack() public payable {
53 timeLock.deposit{value: msg.value}();
54 /*
55 if t = current lock time then we need to find x such that
56 x + t = 2**256 = 0
57 so x = -t
58 2**256 = type(uint).max + 1
59 so x = type(uint).max + 1 - t
60 */
61 timeLock.increaseLockTime(
62 type(uint).max + 1 - timeLock.lockTime(address(this))
63 );
64 timeLock.withdraw();
65 }
66}
Alles anzeigen
Wie man Integer-Unterläufe und -Überläufe verhindert

Ab Version 0.8.0 weist der Solidity-Compiler Code zurück, der zu Integer-Unterläufen und -Überläufen führt. Verträge, die mit einer niedrigeren Compiler-Version kompiliert wurden, sollten jedoch entweder Überprüfungen für Funktionen durchführen, die arithmetische Operationen beinhalten, oder eine Bibliothek (z. B. SafeMath(opens in a new tab)) verwenden, die auf Unterlauf/Überlauf prüft.

Oracle-Manipulation

Oracles beziehen Off-Chain-Informationen und senden sie on-chain, damit Smart Contracts diese nutzen können. Mit Orakeln können Sie Smart Contracts entwerfen, die mit Off-Chain-Systemen wie z. B. Kapitalmärkten interagieren, was ihre Anwendung erheblich erweitert.

Wenn das Orakel jedoch beschädigt ist und falsche Informationen on-chain sendet, werden Smart Contracts auf der Grundlage falscher Informationen ausgeführt, was zu Problemen führen kann. Dies ist die Grundlage des „Orakelproblems“, bei dem es darum geht sicherzustellen, dass die Informationen aus einem Blockchain-Orakel korrekt, aktuell und zeitnah sind.

Ein damit zusammenhängendes Sicherheitsproblem ist die Verwendung eines On-Chain-Orakels, z. B. einer dezentralen Börse, um den Spotpreis für ein Asset zu ermitteln. Leihplattformen in der dezentralen Finanzbranche (DeFi) tun dies oft, um den Wert der Beleihungsobjekte eines Nutzers zu ermitteln, anhand derer er bestimmen kann, wie viel er leihen kann.

Die DEX-Preise sind häufig korrekt, was vor allem darauf zurückzuführen ist, dass Arbitrageure die Gleichheit auf den Märkten wiederherstellen. Sie sind jedoch anfällig für Manipulationen, insbesondere wenn das On-Chain-Orakel die Preise von Assets auf der Grundlage historischer Handelsdaten berechnet (was normalerweise der Fall ist).

So könnte ein Angreifer beispielsweise den Spotpreis eines Assets künstlich in die Höhe treiben, indem er einen Blitzkredit aufnimmt, kurz bevor er mit Ihrem Kreditvertrag interagiert. Die Abfrage der DEX nach dem Preis des Assets würde einen höheren als den normalen Wert ergeben (da die große „Kaufbestellung“ des Angreifers die Nachfrage nach dem Asset verzerrt), so dass er mehr Geld leihen kann, als er sollte. Solche „Flash-Darlehensangriffe“ wurden genutzt, um das Vertrauen in Preis-Orakel bei DeFi-Anwendungen auszunutzen, was Protokolle Millionen an verlorenen Guthaben gekostet hat.

So verhindert man Orakelmanipulation

Die Mindestanforderung zur Vermeidung von Orakelmanipulationen ist die Verwendung eines dezentralen Orakelnetzes, das Informationen aus mehreren Quellen abfragt, um einzelne Fehlerquellen zu vermeiden. In den meisten Fällen verfügen dezentrale Orakel über eingebaute kryptoökonomische Anreize, die die Nodes des Orakels dazu bringen, korrekte Informationen zu melden, was sie sicherer macht als zentralisierte Orakel.

Wenn Sie vorhaben, ein On-Chain-Orakel für Asset-Preise abzufragen, sollten Sie ein Orakel verwenden, das einen Mechanismus für zeitgewichtete Durchschnittspreise (TWAP) implementiert. Ein TWAP-Orakel(opens in a new tab) fragt den Preis eines Assets zu zwei verschiedenen Zeitpunkten ab (die Sie ändern können) und berechnet den Spotpreis auf der Grundlage des erhaltenen Durchschnitts. Die Wahl längerer Zeiträume schützt Ihr Protokoll vor Preismanipulationen, da große Aufträge, die erst kürzlich ausgeführt wurden, keinen Einfluss auf die Preise der Assets haben können.

Ressourcen zur Sicherheit von Smart Contracts für Entwickler

Tools für die Analyse von Smart Contracts und die Überprüfung der Korrektheit des Codes

  • Testing-Tools und -Bibliotheken - Sammlung von branchenüblichen Tools und Bibliotheken zur Durchführung von Unit-Tests, statischer Analyse und dynamischer Analyse von Smart Contracts.

  • Formale Verifizierungstools - Tools zur Verifizierung der funktionalen Korrektheit in Smart Contracts und zur Überprüfung von Invarianten.

  • Smart Contract-Auditing-Dienste - Liste von Organisationen, die Smart Contract-Auditing-Dienste für Ethereum-Entwicklungsprojekte anbieten.

  • Plattformen zum Aufdecken von Fehlern - Plattformen zur Koordinierung des Aufdeckens von Fehlern und zur Belohnung der verantwortungsvollen Offenlegung kritischer Schwachstellen in Smart Contracts.

  • Fork Checker(opens in a new tab) - Ein kostenloses Tool zur Überprüfung aller verfügbaren Informationen bezüglich Fork-Verträgen.

  • ABI Encoder(opens in a new tab) - Ein frei nutzbarer Online-Service zum Kodieren Ihrer Solidity-Vertragsfunktionen und Konstruktor-Argumente.

Tools für die Überwachung von Smart Contracts

Tools für die sichere Verwaltung von Smart Contracts

Dienstleistungen zur Prüfung von Smart Contracts

Plattformen zum Aufdecken von Fehlern

Veröffentlichungen bekannter Schwachstellen und Exploits von Smart Contracts

Herausforderungen beim Erlernen der Sicherheit von Smart Contracts

Bewährte Praktiken für die Sicherung von Smart Contracts

Tutorials zur Sicherheit von Smart Contracts

  • So schreibt man sichere Smart Contracts

  • So verwenden Sie Slither, um Fehler in Smart Contracts zu finden

  • So finden Sie mit Manticore Fehler in Smart Contract

  • Smart Contract Sicherheitsrichtlinien

  • Wie Sie Ihren Token-Vertrag sicher in beliebige Token integrieren

War dieser Artikel hilfreich?