Deep EVM #9: Huff-Sprachgrundlagen — Makros, Labels und rohe Opcodes
Engineering Team
Was ist Huff?
Solidity ist eine wunderbare Abstraktion — bis sie es nicht mehr ist. Wenn Sie einen Contract brauchen, der in 100 Bytes Runtime-Bytecode passt, Funktionen in O(1) mit einer gepackten Sprungtabelle dispatcht oder 200 Gas auf einem heissen Pfad einspart, brauchen Sie etwas naeher am Metall. Dieses Etwas ist Huff — eine Low-Level-EVM-Assemblersprache mit einem duennen Makrosystem, das Ihnen direkte Kontrolle ueber jeden Opcode gibt.
Huff vs. Yul vs. Solidity
| Merkmal | Solidity | Yul | Huff |
|---|---|---|---|
| Abstraktionsniveau | Hoch | Mittel | Niedrig |
| Typ-System | Stark | Keines | Keines |
| Variablen | Ja | Ja | Nein |
| Stack-Kontrolle | Automatisch | Semi-automatisch | Manuell |
| Bytecode-Groesse | Am groessten | Mittel | Am kleinsten |
In Huff gibt es keine Variablen. Sie arbeiten direkt mit dem Stack und muessen den Zustand jedes Elements im Kopf behalten.
Grundlegende Syntax
Makros — die grundlegende Einheit
#define macro MAIN() = takes(0) returns(0) {
// Funktionsselektor aus Calldata laden
0x00 calldataload 0xe0 shr
// Gegen bekannte Selektoren pruefen
dup1 __FUNC_SIG(transfer) eq transfer jumpi
dup1 __FUNC_SIG(balanceOf) eq balanceOf jumpi
// Kein Match -> revert
0x00 0x00 revert
transfer:
TRANSFER()
balanceOf:
BALANCE_OF()
}
takes() und returns()
takes(n) und returns(m) deklarieren, wie viele Stack-Elemente ein Makro erwartet und wie viele es zuruecklaesst. Dies ist rein deklarativ — der Compiler erzwingt es nicht, aber es dient als Dokumentation:
// Erwartet 2 Stack-Elemente, laesst 1 zurueck
#define macro ADD_TWO() = takes(2) returns(1) {
add // Nimmt 2, gibt 1
}
Konstanten und Jump-Labels
#define constant OWNER_SLOT = FREE_STORAGE_POINTER()
#define constant MAX_SUPPLY = 0x989680 // 10,000,000
#define macro CHECK_OWNER() = takes(0) returns(0) {
caller [OWNER_SLOT] sload eq is_owner jumpi
0x00 0x00 revert
is_owner:
}
Funktionsdispatch in Huff
Im Gegensatz zu Solidity, wo der Compiler den Funktionsdispatch automatisch generiert, muessen Sie ihn in Huff manuell implementieren:
#define function transfer(address,uint256) nonpayable returns (bool)
#define function balanceOf(address) view returns (uint256)
#define macro MAIN() = takes(0) returns(0) {
0x00 calldataload 0xe0 shr // Selektor extrahieren
dup1 __FUNC_SIG(transfer) eq transfer_jump jumpi
dup1 __FUNC_SIG(balanceOf) eq balance_jump jumpi
0x00 0x00 revert
transfer_jump:
TRANSFER()
balance_jump:
BALANCE_OF()
}
Einen einfachen ERC-20 in Huff
Storage-Layout
// Slot 0: totalSupply
// Slot 1: name (String-Pointer)
// mapping(address => uint256) balances: keccak256(address, 2)
// mapping(address => mapping(address => uint256)) allowances: keccak256(spender, keccak256(owner, 3))
#define constant BALANCE_SLOT = 0x02
#define constant ALLOWANCE_SLOT = 0x03
Balance-Abfrage
#define macro BALANCE_OF() = takes(0) returns(0) {
// Adresse aus Calldata laden
0x04 calldataload // [address]
// Storage-Slot berechnen: keccak256(address, BALANCE_SLOT)
0x00 mstore // Memory[0x00] = address
[BALANCE_SLOT] 0x20 mstore // Memory[0x20] = 2
0x40 0x00 sha3 // [hash]
sload // [balance]
// Ergebnis zurueckgeben
0x00 mstore
0x20 0x00 return
}
Bytecode-Groessenvergleich
Ein minimaler ERC-20:
- Solidity (OpenZeppelin): ~2.800 Bytes Runtime
- Yul: ~1.200 Bytes Runtime
- Huff: ~600 Bytes Runtime
Weniger Bytecode bedeutet niedrigere Bereitstellungskosten (200 Gas pro Byte) und potenziell bessere Performance, da weniger Code geladen werden muss.
Debugging von Huff
Huff bietet keine Fehlermeldungen zur Laufzeit. Wenn Ihr Contract revertiert, erhalten Sie nur ein leeres Revert-Payload. Debugging-Strategien:
- Stack-Kommentare — Dokumentieren Sie den Stack-Zustand nach jedem Opcode
- Inkrementelles Testen — Jedes Makro einzeln mit Foundry testen
- cast run — Transaktionen auf einer Fork nachspielen und Opcode-Traces analysieren
- forge debug — Schritt-fuer-Schritt durch den Bytecode navigieren
Wann Huff verwenden?
Ja:
- MEV-Bots, bei denen jede Gas-Einheit zaehlt
- Minimale Proxy-Contracts (EIP-1167)
- Vanity-Contracts mit spezifischen Bytecode-Anforderungen
- Wenn Sie die EVM wirklich verstehen wollen
Nein:
- Standard-Geschaeftslogik
- Wenn das Team keine tiefen EVM-Kenntnisse hat
- Wenn Auditierbarkeit prioritaet hat
- Fuer Contracts mit komplexer Logik (die Fehlerquote ist zu hoch)
Fazit
Huff ist die ultimative Low-Level-Sprache fuer die EVM. Kein Compiler-Overhead, kein Typ-System, keine Sicherheitsnetze — nur Sie und die Opcodes. Diese Macht hat ihren Preis: hoeheres Fehlerrisiko und schwierigeres Debugging. Aber fuer leistungskritische Anwendungen wie MEV-Bots ist Huff der Goldstandard.