Gas-Optimierung bei Smart Contracts — Praktische Tipps
Gas ist die Währung der Ausführung auf EVM-Chains. Jede unnötige Operation kostet
Ihre Nutzer Geld. In diesem Beitrag zeigen wir konkrete Optimierungen mit
Vorher/Nachher-Vergleichen. Alle Gas-Werte wurden mit forge test --gas-report
auf Solidity 0.8.24 gemessen.
1. Storage vs. Memory
Ein SSTORE (Schreiben in Storage) kostet 20.000 Gas für einen neuen
Slot bzw. 5.000 Gas für ein Update. Ein MSTORE (Schreiben in Memory)
kostet 3 Gas. Lesen Sie Storage-Variablen in lokale Variablen, wenn Sie sie
mehrfach verwenden:
Vorher (3 Storage-Reads):
function calculateReward(address user) public view returns (uint256) {
// Jeder Zugriff auf balances[user] ist ein SLOAD (2.100 Gas)
if (balances[user] > threshold) {
return balances[user] * rewardRate / 10000;
}
return balances[user] / 2;
}
// Gas: 6.847
Nachher (1 Storage-Read):
function calculateReward(address user) public view returns (uint256) {
uint256 bal = balances[user]; // 1x SLOAD
if (bal > threshold) {
return bal * rewardRate / 10000;
}
return bal / 2;
}
// Gas: 2.891 (-57,8 %)
2. Struct Packing
Die EVM arbeitet mit 32-Byte-Slots. Variablen, die zusammen in einen Slot passen,
werden in einem einzigen SLOAD gelesen. Die Reihenfolge der
Struct-Felder bestimmt, wie viele Slots belegt werden:
Vorher (3 Slots = 3 × 32 Bytes):
struct UserInfo {
uint256 balance; // Slot 0 (32 Bytes)
bool isActive; // Slot 1 (1 Byte, aber belegt ganzen Slot)
uint256 lastClaim; // Slot 2 (32 Bytes)
}
// Deployment: 142.308 Gas
// setUser(): 67.411 Gas
Nachher (2 Slots):
struct UserInfo {
uint256 balance; // Slot 0
uint128 lastClaim; // Slot 1 (16 Bytes) — uint128 reicht bis Jahr 10^19
bool isActive; // Slot 1 (1 Byte, packt in selben Slot)
}
// Deployment: 118.592 Gas (-16,7 %)
// setUser(): 45.223 Gas (-32,9 %)
3. calldata vs. memory
Für external-Funktionen, die Array- oder Struct-Parameter nur lesen
(nicht modifizieren), verwenden Sie calldata statt memory.
memory kopiert die Daten; calldata liest direkt aus den
Transaktionsdaten.
// Vorher: 35.124 Gas
function processIds(uint256[] memory ids) external {
for (uint256 i = 0; i < ids.length; i++) {
emit Processed(ids[i]);
}
}
// Nachher: 34.201 Gas (-2,6 %)
function processIds(uint256[] calldata ids) external {
for (uint256 i = 0; i < ids.length; i++) {
emit Processed(ids[i]);
}
}
Die Ersparnis steigt mit der Größe des Arrays. Bei 100 Elementen beträgt sie bereits ca. 8 %.
4. unchecked-Blöcke für Schleifenzähler
Ab Solidity 0.8 prüft der Compiler bei jeder Arithmetik auf Overflow. Für
Schleifenzähler, die beweisbar nicht überlaufen können, spart
unchecked Gas:
// Vorher: 48.312 Gas (100 Iterationen)
for (uint256 i = 0; i < 100; i++) {
total += values[i];
}
// Nachher: 43.879 Gas (-9,2 %)
for (uint256 i = 0; i < 100; ) {
total += values[i];
unchecked { ++i; }
}
Beachten Sie: ++i (Präfix) ist minimal günstiger als i++
(Postfix), da kein temporärer Wert gespeichert wird.
5. Events statt Storage für historische Daten
Wenn Daten nur off-chain ausgelesen werden müssen (z. B. für ein Frontend), sind Events drastisch günstiger als Storage:
// Vorher: Storage-Array — 20.000+ Gas pro Eintrag
mapping(address => uint256[]) public userHistory;
function recordAction(uint256 actionId) external {
userHistory[msg.sender].push(actionId);
}
// Nachher: Event — 375 Gas Basis + 375 Gas pro indexed Topic
event ActionRecorded(address indexed user, uint256 actionId);
function recordAction(uint256 actionId) external {
emit ActionRecorded(msg.sender, actionId);
}
Einsparung: > 95 %. Events werden im Transaction-Log gespeichert und sind
über eth_getLogs abrufbar, aber nicht von anderen Contracts lesbar.
6. Konstanten und Immutables
// Variable: 2.100 Gas (SLOAD)
uint256 public fee = 250;
// constant: 0 Gas (wird zur Compile-Zeit inline ersetzt)
uint256 public constant FEE = 250;
// immutable: 0 Gas zur Laufzeit (im Bytecode)
uint256 public immutable deploymentTime;
constructor() {
deploymentTime = block.timestamp;
}
Verwenden Sie constant für Werte, die zur Compile-Zeit bekannt sind,
und immutable für Werte, die im Constructor gesetzt werden.
Zusammenfassung
Optimierung | Typische Einsparung
---------------------------|--------------------
Storage-Reads cachen | 40-60 %
Struct Packing | 15-35 %
calldata statt memory | 2-8 %
unchecked Schleifenzähler | 8-12 %
Events statt Storage | > 95 %
constant/immutable | 100 % (kein SLOAD)
Jede einzelne Optimierung mag gering erscheinen. In einem Contract mit 20
Funktionen und tausenden täglichen Aufrufen summiert sich das zu signifikanten
Einsparungen für Ihre Nutzer. Messen Sie immer mit forge test --gas-report
— optimieren Sie nicht auf Basis von Annahmen.