
Einleitung
Smart Contracts sind so programmiert, dass sie nach ihrer Bereitstellung in einem Blockchain-Netzwerk dauerhaft und unveränderlich sind. Diese Unveränderlichkeit sorgt für Sicherheit und Vertrauen, stellt Entwickler jedoch vor Herausforderungen, wenn sie Fehler beheben oder neue Funktionen hinzufügen müssen.
Wenn Änderungen nötig sind, müssen Entwickler einen komplett neuen Vertrag mit einer anderen Adresse veröffentlichen. Diese Beständigkeit ist zwar super für die Sicherheit, kann aber in Situationen, in denen etwas schiefgeht oder verbessert werden muss, einschränkend sein.
Die Geschichte hat gezeigt, dass Schwachstellen in Smart Contracts zu katastrophalen finanziellen Verlusten führen können. Ein besonderes Beispiel ist ein schwerwiegender Vorfall, bei dem aufgrund einer ausnutzbaren Schwachstelle Millionen von Dollar verloren gingen.
Diese Vorfälle zeigen, wie wichtig es ist, Sicherheitsprobleme und Fehler nach der Bereitstellung beheben zu können. Durch die Möglichkeit, Verträge zu aktualisieren, können Probleme behoben werden, ohne den bestehenden Zustand und die Benutzererfahrung zu beeinträchtigen, was ein wichtiger Aspekt der Aktualisierbarkeit von Smart Contracts ist.
Diese Fähigkeit kann wichtig sein, um erhebliche Verluste zu vermeiden und die Integrität des Systems zu bewahren.
Es gibt verschiedene Möglichkeiten, aktualisierbare Smart Contracts zu implementieren, z. B. Smart Contract Proxy-Muster und Datentrennungstechniken. Dieser Artikel befasst sich speziell mit der Implementierung der Aktualisierbarkeit mithilfe von Proxy-Mustern, einer beliebten Methode zur Lösung dieses Problems im Bereich der Blockchain-Entwicklung.
Das Proxy-Muster verstehen
Das Proxy-Muster ist ein strukturelles Entwurfsmuster, bei dem ein Vertrag eine Schnittstelle für einen anderen Vertrag implementiert. Diese Architektur besteht aus zwei Hauptteilen: dem Proxy-Vertrag und dem Implementierungsvertrag.
Anstatt direkt mit dem Implementierungsvertrag zu kommunizieren, kommuniziert der Benutzer mit dem Proxy-Vertrag, der die Anfragen dann entsprechend weiterleitet.
In dieser Konfiguration werden sowohl der Proxy-Vertrag als auch der Implementierungsvertrag nach ihrer Bereitstellung nicht mehr geändert. Die Aktualisierbarkeit wird jedoch dadurch erreicht, dass der Proxy im Laufe der Zeit auf verschiedene Implementierungsverträge verweisen kann.
Das heißt, die Nutzer können die gleiche Adresse und Oberfläche weiter benutzen, während die Funktionen im Hintergrund aktualisiert werden. Aus Sicht der Nutzer läuft die Anwendung also weiter einwandfrei, auch wenn sich die Backend-Logik ändert.
Der Proxy-Vertrag ist für die Interaktionen der Benutzer und die Speicherung aller Daten zuständig. Er speichert die Adresse des Implementierungsvertrags an einem speziellen Speicherort.
Wenn Benutzer Funktionen im Proxy-Vertrag aufrufen, wird eine Fallback-Funktion aufgerufen. Die folgende Funktion nutzt einen Delegatecall-Ethereum-Mechanismus, um den Code aus dem Implementierungsvertrag aufzurufen, aber alle Statusänderungen werden im Speicher des Proxy-Vertrags gespeichert.
Wichtige Proxy-Muster-Implementierungen
Transparentes Proxy-Muster
Das Transparent Proxy Pattern legt die Upgrade-Funktionalität im Proxy-Vertrag selbst ab. Der Proxy verfügt über eine upgradeTo-Methode, mit der die Adresse aktualisiert wird, die auf den Implementierungsvertrag verweist.
Das kann ein Problem sein: Wenn sowohl der Proxy- als auch der Implementierungsvertrag eine upgradeTo-Methode haben, ist nicht klar, welche aufgerufen werden soll, wenn jemand diese Funktion nutzt.
Um diese Unklarheit zu beseitigen, wurde eine Lösung entwickelt, bei der die Entscheidung, an wen die Aufgabe delegiert wird, davon abhängt, wer den Vertrag aufruft.
Wenn der Aufrufer der Proxy-Administrator ist, werden die Aufrufe vom Proxy-Vertrag selbst abgewickelt. Ansonsten werden die Aufrufe an den Implementierungsvertrag weitergeleitet. Dieser Ansatz stellt sicher, dass Funktionen klar ausgeführt werden und es keine Verwirrung zwischen administrativen und regulären Benutzeroperationen gibt.
Universeller aktualisierbarer Proxy-Standard
Der Universal Upgradeable Proxy Standard geht anders vor, indem er die Upgrade-Funktion in den Implementierungsvertrag statt in den Proxy packt. Der Proxy leitet alle Aufrufe an den Implementierungsvertrag weiter, der über die Methode „upgradeTo“ verfügt, um auf neuere Versionen zu verweisen.
Dieses Muster gibt Entwicklern mehr Kontrolle über den Upgrade-Pfad. Wenn ein Entwickler beschließt, die upgradeTo-Methode in einer zukünftigen Version nicht mehr zu verwenden, ist der Vertrag nun dauerhaft unveränderlich und kann nicht weiter aktualisiert werden.
Das kann nützlich sein, wenn ein Vertrag gut getestet wurde und das Entwicklungsteam ihn in seinem endgültigen Zustand einfrieren will.
Da die Upgrade-Logik im Implementierungsvertrag steht, muss die Fallback-Funktion des Proxys nicht überprüfen, ob der Aufrufer ein Administrator ist, bevor sie Aufrufe weiterleitet.
Dadurch ist dieses Muster gas-effizienter als das Transparent Proxy Pattern. Auch mögliche Namenskonflikte werden vermieden, da die upgradeTo-Methode nur im Implementierungsvertrag vorkommt.
Beacon-Proxy-Muster
Das Beacon-Proxy-Muster hat eine Architektur mit drei Teilen: den Proxy-Vertrag, einen Beacon-Vertrag und den Implementierungsvertrag. Anstatt die Adresse der Implementierung zu speichern, speichert der Proxy die Adresse des Beacon-Vertrags.
Der Beacon-Vertrag enthält wiederum die Adresse des Implementierungsvertrags.
Wenn ein Benutzer den Proxy aufruft, ruft dieser zunächst die Adresse des Beacon-Vertrags ab und ruft dann den Beacon auf, um die Adresse des Implementierungsvertrags abzurufen. Schließlich wird der Aufruf an den Implementierungsvertrag weitergeleitet.
Diese zusätzliche Ebene der Indirektheit dient einem ganz bestimmten Zweck.
Dieses Muster ist besonders nützlich, wenn mehrere Proxy-Verträge dieselbe Implementierung verwenden müssen. Bei einfacheren Mustern müsste man zur Aktualisierung der Implementierung die Adresse in jedem Proxy-Vertrag einzeln aktualisieren. Mit dem Beacon-Muster muss nur der Beacon-Vertrag aktualisiert werden, und alle damit verbundenen Proxys verweisen automatisch auf die neue Implementierung.
Wichtige Proxy-Muster-Implementierungen
Diamond-Proxy-Muster
Das Diamond Proxy Pattern löst das Problem der Größenbeschränkung von Smart Contracts, die normalerweise auf etwa 24 Kilobyte begrenzt sind. Dieses Muster teilt die Funktionalität in mehrere kleinere Verträge auf, die als Facetten bezeichnet werden.
Der Proxy-Vertrag behält eine Zuordnung zwischen Funktionsselektoren und der Adresse der Facetten bei, die diese Funktionen enthalten.
Wenn eine Funktion auf dem Proxy aufgerufen wird, sucht sie mit dem Funktionsselektor nach der Facette, die die Funktion enthält, und leitet den Funktionsaufruf an die richtige Facette weiter.
Upgrades werden durch Ändern der Facettenadressen im Proxy durchgeführt. Dieses Muster ermöglicht eine viel größere Gesamtfunktionalität, indem die Funktionalität auf mehrere Verträge verteilt wird, wobei jede Facette unter der Größenbeschränkung bleibt.
Vergleich von Proxy-Muster-Ansätzen
Jedes Proxy-Muster hat unterschiedliche Eigenschaften, die es für verschiedene Anwendungsfälle geeignet machen. Alle vier Muster brauchen einen Proxy-Vertrag und nutzen Delegation, um Aufrufe an Implementierungsverträge weiterzuleiten.
Das Transparent Proxy Pattern findet Upgrade-Funktionen im Proxy-Vertrag und UUPS findet Upgrade-Funktionen im Implementierungsvertrag. Das Beacon-Muster packt die Upgrade-Fähigkeit in einen eigenen Beacon-Vertrag und das Diamond-Muster packt die Upgrade-Fähigkeit normalerweise in die Implementierungsverträge, aber das ist nicht streng festgelegt.
Was die Unveränderlichkeit angeht, können UUPS- und Diamond-Muster Verträge dauerhaft unveränderlich machen, indem sie die Upgrade-Funktion aus zukünftigen Versionen entfernen. Transparent- und Beacon-Muster bieten diese Flexibilität nicht.
Wenn du mit mehreren Proxys arbeitest, ist das Beacon-Muster echt praktisch. Bei den Mustern Transparent, UUPS und Diamond musst du jeden Proxy einzeln anpassen, wenn du eine neue Implementierung machst.
Beim Beacon-Muster musst nur der Beacon-Vertrag aktualisiert werden, und alle Proxys nutzen dann automatisch die neue Implementierung.
Die Gas-Effizienz ist bei jedem Muster anders. Beim Transparent-Muster musst du vor jeder Delegierung checken, ob der Aufrufer ein Administrator ist, was es teurer macht.
UUPS ist effizienter, da es diese Überprüfung nicht braucht. Alle Muster außer Diamond haben zusätzliche Gas-Kosten für die zusätzlichen Suchvorgänge.
Alle Muster außer Diamond sind auf 24 Kilobyte pro Vertrag begrenzt. Beim Diamond-Muster kann jede Facette bis zu 24 Kilobyte groß sein, was eine viel größere Gesamtfunktionalität über mehrere Facetten hinweg ermöglicht.
Meistere die Sicherheit von Smart Contracts
Lerne in unseren Kursen mit Experten fortgeschrittene Techniken, um sichere, aktualisierbare Verträge zu erstellen.
Wichtige Überlegungen und Risiken
Probleme mit Speicherkonflikten
Speicherkollisionen sind ein echtes Risiko bei der Implementierung von Proxy-Mustern. Vertragsvariablen werden in bestimmten Speicherplätzen gespeichert, und wenn die Variablen des Proxy-Vertrags und die Variablen des Implementierungsvertrags in denselben Speicherplätzen gespeichert werden, stören sie sich gegenseitig.
Wenn sich außerdem die Reihenfolge der Variablen im Implementierungsvertrag von einer Version zur nächsten ändert, können die Speicherplätze neu zugewiesen werden, was zu Datenkorruption führen kann.
Nicht initialisierte Implementierungsverträge
Implementierungsverträge müssen genau einmal über eine Initialisierungsfunktion initialisiert werden, was einem Konstruktor in herkömmlichen Verträgen ähnelt.
Entwickler vergessen auch, die Implementierung zu initialisieren, oder vergessen, Schutzmaßnahmen einzubauen, die verhindern, dass die Initialisierungsfunktion mehr als einmal aufgerufen wird.
In diesem Fall können Angreifer die Initialisierungsfunktion selbst aufrufen und möglicherweise die Kontrolle über den Vertrag übernehmen oder dessen Status manipulieren.
Sicherheitsvorfälle aus der Praxis
Ein Bug-Bounty-Programm hat eine kritische Schwachstelle in Vault-Proxy-Verträgen gefunden, bei denen die Implementierungsverträge nicht richtig initialisiert wurden. Diese Schwachstelle hätte von einem Angreifer ausgenutzt werden können, um den Implementierungsvertrag zu zerstören und die damit verbundenen Proxy-Verträge unbrauchbar zu machen. In einem anderen Vorfall im Juli wurden Smart Contracts aufgrund einer Schwachstelle im Initialisierungscode gehackt, bei der die Initialisierungsfunktion mehrfach aufgerufen werden konnte.
Alternative Ansätze zur Aufrüstbarkeit
Muster für die Datentrennung
Eine Alternative zu den Proxy-Mustern ist der Ansatz der Datentrennung. Dieser Ansatz basiert auf der Verwendung separater Verträge für Speicherung und Logik. Der Logikvertrag kommuniziert mit dem Speichervertrag, um Daten zu lesen oder zu aktualisieren.
Es ist zwar möglich, den Logikvertrag durch neue Versionen zu ersetzen, aber der Speichervertrag ist fest und kann nicht geändert werden. Das ist ein anderes Modell für die Aktualisierbarkeit, das unter bestimmten Umständen sinnvoll sein kann.
Verifizierungsebenen
Keines der gängigen Proxy-Muster hat Mechanismen, um vor dem Upgrade zu checken, ob ein neuer Implementierungsvertrag gültig ist.
Neue Versionen müssen unbedingt die ganze erforderliche Geschäftslogik, Fallback-Funktionen und andere wichtige Komponenten enthalten.
Das kannst du erreichen, indem du eine Verifizierungsebene einführst, die neben der Proxy-Ebene, der Geschäftslogik-Ebene und der Speicherebene liegt. Diese zusätzliche Ebene stellt sicher, dass Upgrades bestimmte Kriterien erfüllen, bevor sie durchgeführt werden dürfen.
Bewährte Verfahren für aktualisierbare Verträge
Entwickler, die aktualisierbare Verträge entwerfen, sollten ein paar wichtige Richtlinien beachten, um Sicherheit und Zuverlässigkeit zu gewährleisten:
- •Nutze stattdessen bewährte Implementierungen aus gut getesteten Bibliotheken, die gründlich geprüft und auditiert wurden.
- •Stell immer sicher, dass die Implementierungsverträge richtig initialisiert werden und dass Initialisierungsfunktionen nur einmal aufgerufen werden können.
- •Initialisiere Zustandsvariablen niemals bei ihrer Deklaration oder in einem Konstruktor, sondern benutze die Initialisierungsfunktion für alle Zustands-Setups.
- •Ändere nicht die Reihenfolge oder die Arten von Statusvariablen, wenn du neue Versionen von Implementierungsverträgen erstellst.
- •Wenn neue Variablen nötig sind, sollten sie nach allen vorhandenen Variablen hinzugefügt werden.
- •Für Diamond-Muster-Implementierungen solltest du spezielle Speichermethoden verwenden, die für dieses Muster optimiert sind.
- •Die UUPS-Methode ist nach Möglichkeit dem Transparent Proxy Pattern vorzuziehen, da sie für Routinevorgänge weniger Gas benötigt.
- •Stell sicher, dass das Proxy-Administratorkonto super sicher ist, weil dieses Konto den Upgrade-Prozess steuert und ein wichtiger Sicherheitspunkt ist.
- •Lass schließlich alle Verträge von erfahrenen Smart-Contract-Sicherheitsexperten professionell prüfen, bevor du sie einsetzt.
Abschließende Gedanken
Proxy-Muster bieten einen leistungsstarken Mechanismus für die Aktualisierung von Smart Contracts, wobei die gleiche Adresse und der gleiche Status beibehalten werden. Der Proxy-Vertrag verwendet delegateCall, um die Ausführung an Implementierungsverträge weiterzugeben, sodass die zugrunde liegende Logik geändert werden kann, ohne die Benutzeroberfläche zu modifizieren.
Wenn aktualisierbare Verträge aber nicht richtig umgesetzt werden, können sie zu ernsthaften Sicherheitslücken führen.
Entwickler müssen die Vor- und Nachteile verschiedener Proxy-Muster sorgfältig abwägen, bewährte Verfahren einhalten und sicherstellen, dass ordnungsgemäße Sicherheitsprüfungen durchgeführt werden, um zuverlässige und sichere, aktualisierbare Smart Contracts zu erstellen.
Vergleich von Proxy-Mustern
Beim Vergleich der verschiedenen Proxy-Muster solltest du mehrere Faktoren berücksichtigen:
Vergleich der Proxy-Muster
| Funktion | Transparent | UUPS | Beacon | Diamant |
|---|---|---|---|---|
| Standort aktualisieren | Proxy-Vertrag | Implementierungsvertrag | Beacon-Vertrag | Implementierungsverträge |
| Dauerhafte Unveränderlichkeit | Nein | Ja | Nein | Ja |
| Mehrere Proxys | Einzelne Aktualisierungen | Einzelne Aktualisierungen | Einzelnes Beacon-Update | Einzelne Aktualisierungen |
| Kraftstoffeffizienz | Höhere Kosten (Administratorprüfung) | Geringere Kosten | Medium (zusätzliche Suche) | Medium (Facetten-Suche) |
| Vertragsgrößenbeschränkung | 24 KB | 24 KB | 24 KB | 24 KB pro Facette |
| Komplexität der Umsetzung | Medium | Medium | Medium | Medium |
Überlegungen zur Musterauswahl
Wichtige Überlegungen zur Musterauswahl:
- •Alle Muster brauchen einen Proxy-Vertrag und nutzen Delegation, um Anrufe weiterzuleiten.
- •Das Transparent Proxy Pattern wird verwendet, um die Upgrade-Funktionen im Proxy-Vertrag selbst zu speichern.
- •UUPS fügt die Upgrade-Funktionen in den Implementierungsvertrag ein.
- •Das Beacon-Muster nutzt einen separaten Beacon-Vertrag für Upgrades.
- •Das Diamond-Muster sieht normalerweise vor, dass Upgrade-Funktionen in Implementierungsverträgen stehen, aber die Spezifikation verlangt das nicht.
- •Nur UUPS- und Diamond-Muster haben die Möglichkeit, Verträge dauerhaft unveränderlich zu machen, indem die Upgrade-Funktion aus zukünftigen Versionen entfernt wird.
- •Das Beacon-Muster ist super, wenn du mehrere Proxys gleichzeitig aktualisieren musst, weil nur das Beacon aktualisiert werden muss und nicht jeder einzelne Proxy.
- •Die maximale Vertragsgröße beträgt 24 Kilobyte für Transparent-, UUPS- und Beacon-Muster.
- •Das Diamond-Muster unterstützt jede Facette mit einer Größe von bis zu 24 Kilobyte, sodass eine viel größere Gesamtfunktionalität unterstützt werden kann.
- •Alle Muster haben eine mittlere Komplexität bei der Umsetzung, und es gibt gut etablierte Bibliotheken für Transparent-, UUPS- und Beacon-Muster.


