Zum Inhalt springen

Proxy-Speicherkollision: Wie Upgrades den Kontraktzustand beschädigen

Ein Proxy behält seinen Zustand, während sich der Implementierungscode ändert. Erfahren Sie, wie inkompatible Layouts Guthaben, Eigentümer und Upgrade-Kontrollen überschreiben und wie man Upgrades prüft.

Aktualisiert

Nur zu Bildungszwecken; keine Anlageberatung. Investitionen können zu Verlusten führen.

Direkte Antwort

Eine Proxy-Speicherkollision entsteht, wenn Implementierungscode einen Speicher-Slot des Proxys mit einer anderen Bedeutung liest oder schreibt als das Layout, das den vorhandenen Zustand erzeugt hat. Ein Upgrade kann dadurch einen Kontostand als Adresse deuten, den Eigentümer löschen, den Basis-Slot eines Mappings beschädigen oder Upgrade-Steuerdaten überschreiben.

Bei delegatecall läuft der Bytecode der Implementierung im Kontext des Proxys: Speicher, Guthaben und address(this) gehören zum Proxy. Variablennamen werden nicht on-chain gespeichert; die EVM folgt nur dem Slot und Byte-Offset, die der neue Code berechnet.

Ein Upgrade muss daher das bereitgestellte Layout erhalten und darf nicht nur erfolgreich kompilieren oder dieselben Funktionen anbieten. Vor der Freigabe sind die vom Compiler erzeugten Layouts mit der exakt bereitgestellten Version zu vergleichen.

Funktionsweise

Solidity legt Zustandsvariablen normalerweise nach C3-Linearisierung der Vererbung in Deklarationsreihenfolge ab Slot 0 ab. Werte unter 32 Byte können einen Slot teilen; Structs und Arrays folgen weiteren Regeln, während Mappings und dynamische Arrays ihre Datenposition aus einem Basis-Slot ableiten. Verschiebt sich dieser, verschieben sich auch die abgeleiteten Daten.

Vier unterschiedliche Kollisionsgrenzen sind zu prüfen:

  • Proxy gegenüber Implementierung: Proxy-eigene Felder wie Implementierung oder Administrator dürfen keine Slots des Anwendungszustands belegen. ERC-1967 definiert standardisierte, vom Compiler gemiedene Slots für Implementierung, Beacon und Administrator.
  • Alte gegenüber neuer Implementierung: Bestehende Variablen müssen kompatible Slots, Offsets und Typen behalten. Anhängen kann sicher sein; Einfügen, Umsortieren, Löschen oder Typänderungen können vorhandene Wörter neu deuten.
  • Vererbung: Neuer Zustand in einem Basiskontrakt oder eine andere Vererbungsreihenfolge kann Speicher der Nachfahren verschieben, obwohl deren Quellcode unverändert ist.
  • Reservierter oder namensbasierter Speicher: Ein korrekt genutzter Storage-Gap reserviert Platz für einen Basiskontrakt; ERC-7201-Namensräume isolieren Layouts. Keine Technik erlaubt beliebige Änderungen innerhalb eines bestehenden Layouts.

Beispiel

Angenommen, Version 1 hat dieses Layout:

uint256 totalAssets; // slot 0
address owner;       // slot 1

Version 2 fügt fälschlich am Anfang eine Variable ein:

bool paused;         // slot 0, offset 0
uint256 totalAssets; // slot 1
address owner;       // slot 2

Nach dem Upgrade liest paused das niederwertige Byte des alten totalAssets, das neue totalAssets deutet das alte owner-Wort als Ganzzahl, und owner liest den vorhandenen Wert in Slot 2, meist null. Die ursprünglichen Wörter bleiben gespeichert, erhalten aber durch den neuen Code andere Bedeutungen. Eine Transaktion kann erfolgreich sein und dennoch Autorisierung oder Buchführung auf beschädigte Deutungen anwenden.

Risiken und Upgrade-Prüfungen

  • Erzeugen Sie das Speicherlayout beider Implementierungen und vergleichen Sie Slot, Offset, Typ und Vererbung mit dem tatsächlich bereitgestellten Referenzkontrakt.
  • Hängen Sie bei linearen Layouts neue Variablen nur an. Ordnen Sie Felder nicht um, ändern Sie keine Typen, löschen und verwenden Sie sie nicht neu und ändern Sie keine Basiskontrakte ohne Kompatibilitätsnachweis.
  • Verkleinern Sie einen Storage-Gap exakt um die Anzahl genutzter reservierter Slots. Halten Sie Namensraum-IDs eindeutig und validieren Sie Änderungen in jedem bestehenden Namensraum.
  • Betrachten Sie ERC-1967 nicht als vollständigen Schutz. Es trennt Proxy-Metadaten von Compiler-Slots der Anwendung, macht aber zwei Implementierungslayouts nicht kompatibel.
  • Testen Sie Upgrade und Reinitialisierung auf einem Fork oder Snapshot. Prüfen Sie davor und danach Eigentümer, Rollen, Guthaben, Allowances, Mapping-Einträge, Pausenstatus, Implementierungs-Slots sowie Rollback- und Notfallkontrollen.

Wenn ein Upgrade bereits eine Kollision verursacht haben könnte, stoppen Sie soweit möglich weitere Upgrades und zustandsändernde Aufrufe. Sichern Sie Blocknummer und Bytecode vor dem Upgrade, vergleichen Sie Rohspeicher betroffener Slots und lassen Sie eine Migration fachkundig entwerfen und unabhängig prüfen. Wiederholte Upgrades ohne belegte Speicherkarte können weiteren wiederherstellbaren Zustand zerstören.

Häufige Irrtümer

  • „Die Variablennamen sind gleich, also ist das Layout sicher.“ Namen bestimmen keine Speicherpositionen. Typen, Reihenfolge, Packung, Vererbung und Namensräume tun dies.
  • „Das Löschen einer Variablen gibt ihren Slot frei.“ Proxy-Speicher bleibt bestehen. Wiederverwendung gibt dem alten Wort eine neue Bedeutung, sofern eine geprüfte Migration es nicht löscht oder umwandelt.
  • „Eine erfolgreiche Testtransaktion beweist Kompatibilität.“ Sie berührt vielleicht nur wenige Slots. Layout- und Zustandsdifferenztests müssen privilegierte Felder, gepackte Werte, Mappings, Arrays und geerbten Speicher abdecken.

Verwandte Themen

Quellen

Navigation

Wiki durchsuchen...