Das Proxy-Problem — Warum Monolithe nicht skalieren
"Ein Smart Contract ist unveränderlich. Bis er es nicht mehr ist."
1.1 Das Versprechen der Unveränderlichkeit
Smart Contracts auf Ethereum sind by design unveränderlich: Einmal deployed, kann kein Code mehr geändert werden. Das ist eine Stärke — Nutzer können dem Vertrag vertrauen, weil niemand ihn heimlich ändern kann.
Aber in der Praxis brauchen Protokolle Upgrades: Bugs müssen gefixt, Features hinzugefügt und Business-Logik weiterentwickelt werden.
Folgen des Monolithen:
- Willkürliche Aufteilung in mehrere Verträge
- Komplex verkettete Aufrufe (hoher Gas-Overhead)
- Storage muss über Verträge koordiniert werden
1.3 Die Proxy-Familie: Drei Generationen
Gen 1: Einfacher Proxy
Proxy speichert State, Implementation enthält Logik.
❌ Storage-Kollisionen (Slot 0)
Gen 2: Transparent Proxy
Implementation-Adresse in Pseudo-Zufalls-Slot (EIP-1967).
⚠️ Zwei Aufrufer-Klassen, ein 24KB-Limit
Gen 3: UUPS (EIP-1822)
Upgrade-Logik wandert in die Implementation.
⚠️ Single Point of Failure bei Upgrades
1.5 Die Einsicht hinter dem Diamond
Was, wenn wir statt einer Implementation viele haben — und den Aufruf zum richtigen Stück Logik routen?
0x2e1a7d4d (withdraw)
delegatecall(facet, msg.data);
Jede Facette ist ein eigener Vertrag mit eigenem Bytecode. Der Diamond ist nur ein Router. Das 24KB-Limit existiert nicht mehr als Systemgrenze — nur als Limit pro Facette.
EIP-2535 — Die Spezifikation Schritt für Schritt
"A diamond is a contract with external functions that are supplied by contracts called facets."
— Nick Mudge, EIP-2535
2.1 Die drei Kerninterfaces
EIP-2535 definiert drei Pflicht-Interfaces. Wer alle drei implementiert, hat einen Diamond.
IDiamond
Definiert FacetCutAction (Add, Replace, Remove) und das DiamondCut-Event für on-chain Nachvollziehbarkeit.
IDiamondCut
Fügt Funktionen hinzu, ersetzt oder entfernt sie. Ermöglicht State-Migrationen via _init Call.
IDiamondLoupe
Macht den Diamond vollständig introspektierbar. Ohne Off-Chain Datenbanken kann jeder Zustand ausgelesen werden.
2.2 Der Selektor: Das Routing-Herz
Jede Solidity-Funktion hat einen 4-Byte-Selektor, der aus dem Keccak256-Hash der Signatur berechnet wird. Der Diamond hält eine interne Mapping-Tabelle (selectorToFacet).
2.3 Die fallback()-Funktion: Das Herz des Routings
fallback() external payable {
LibDiamond.DiamondStorage storage ds;
bytes32 position = LibDiamond.DIAMOND_STORAGE_POSITION;
assembly { ds.slot := position }
address facet = ds.selectorToFacetAndPosition[msg.sig].facetAddress;
require(facet != address(0), "Diamond: Function does not exist");
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0)
...
}
}
Wichtig: delegatecall bedeutet, dass die Facette im Storage-Kontext des Diamonds ausgeführt wird. Der Diamond ist der Datenspeicher, die Facetten sind die Logik-Provider.
2.4 Die drei FacetCutAction-Operationen
address(0))