Zum Hauptinhalt springen
5 Min. Lesezeit R.S.

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.