Aller au contenu principal
BlockchainMar 28, 2026

Deep EVM #9 : Introduction à Huff — Macros, labels et opcodes bruts

OS
Open Soft Team

Engineering Team

Pourquoi Huff existe

Solidity est une abstraction merveilleuse — jusqu’à ce qu’elle ne le soit plus. Quand vous avez besoin d’un contrat qui tient dans 100 octets de bytecode runtime, qui dispatche les fonctions en O(1) avec une table de saut compacte, ou qui économise 200 gas sur un chemin critique exécuté des millions de fois par jour, vous avez besoin de quelque chose plus proche du métal. Ce quelque chose, c’est Huff.

Huff est un langage d’assemblage EVM de bas niveau avec un système de macros léger ajouté par-dessus. Il n’a pas de variables, pas de types, pas de compilateur qui optimise dans votre dos. Ce que vous écrivez est ce qui finit on-chain — opcode par opcode.

Installer Huff

Le compilateur canonique est huffc, écrit en Rust :

curl -L get.huff.sh | bash
huffup
huffc --version

Cela installe huffc dans ~/.huff/bin. Ajoutez-le à votre PATH et vérifiez :

$ huffc --version
huffc 0.3.2

Vous pouvez aussi utiliser Huff dans les projets Foundry avec foundry-huff, qui permet de déployer des fichiers .huff de la même manière que des fichiers .sol.

Hello World : un contrat minimal

Écrivons un contrat qui retourne le mot de 32 octets 0x01 à tout appel :

#define macro MAIN() = takes(0) returns(0) {
    0x01            // [0x01]
    0x00            // [0x00, 0x01]
    mstore          // []          — memory[0x00..0x20] = 0x01
    0x20            // [0x20]
    0x00            // [0x00, 0x20]
    return          // halt — retourner memory[0x00..0x20]
}

Compilez :

huffc src/HelloWorld.huff -r

Le flag -r produit le bytecode runtime. Vous obtiendrez quelque chose comme 600160005260206000f3 — 10 octets. Un contrat Solidity retournant 1 compile en environ 200+ octets de bytecode runtime parce que solc émet un dispatcher complet, un hash de métadonnées, la configuration du pointeur de mémoire libre et un encodeur ABI.

Macros vs fonctions

Huff a deux primitives de réutilisation de code : les macros et les fonctions.

Macros (#define macro)

Les macros sont inlinées à chaque site d’appel. Pas d’overhead de JUMP, pas de gas supplémentaire — le compilateur copie littéralement les opcodes dans l’appelant. C’est le choix par défaut et préféré pour le code critique en gas.

#define macro REQUIRE_NOT_ZERO() = takes(1) returns(0) {
    // takes: [value]
    continue        // [continue_dest, value]
    jumpi           // []  — sauter si value != 0
    0x00 0x00 revert
    continue:
}

Fonctions (#define fn)

Les fonctions génèrent une vraie paire JUMP/JUMPDEST. Elles économisent la taille du bytecode au prix d’environ 22 gas supplémentaires par appel. Utilisez-les uniquement quand la taille du bytecode compte plus que le gas.

Labels et destinations de saut

Les labels en Huff sont des emplacements JUMPDEST nommés. Le compilateur les résout en offsets concrets dans le bytecode à la compilation.

#define macro LOOP_EXAMPLE() = takes(1) returns(1) {
    // takes: [n]
    0x00                // [acc, n]
    loop:
        dup2            // [n, acc, n]
        iszero          // [n==0?, acc, n]
        done jumpi      // [acc, n]
        swap1           // [n, acc]
        0x01 swap1 sub  // [n-1, acc]
        swap1           // [acc, n-1]
        0x01 add        // [acc+1, n-1]
        loop jump
    done:
        swap1 pop       // [acc]
}

Chaque label compile en un seul octet JUMPDEST (0x5b). Les références compilent en PUSH2 <offset> JUMP (ou JUMPI). C’est exactement ce que vous écririez à la main en assembleur EVM brut — Huff gère simplement la comptabilité des offsets.

takes() et returns()

Les annotations takes(n) et returns(m) sur les macros et fonctions sont de la documentation et des indices pour le compilateur. Elles indiquent — au lecteur et au vérificateur de pile de Huff — combien d’éléments de pile le bloc s’attend à consommer et produire.

#define macro ADD_TWO() = takes(2) returns(1) {
    add  // consomme 2 éléments, en produit 1
}

Comparaison : Huff vs bytecode Solidity

Considérons une simple fonction vue getValue() qui retourne un slot de stockage :

Solidity :

function getValue() external view returns (uint256) {
    return value;
}

Solc génère environ 40 octets pour le dispatcher + encodage ABI.

Équivalent Huff :

#define function getValue() view returns (uint256)

#define macro GET_VALUE() = takes(0) returns(0) {
    [VALUE_SLOT]    // [slot]
    sload           // [value]
    0x00 mstore     // []  — stocker en mémoire
    0x20 0x00 return
}

La version Huff fait 12 octets de bytecode pour le corps. Pas d’overhead d’encodage ABI, pas de pointeur de mémoire libre, pas de hash de métadonnées.

Constantes et slots de stockage

Les constantes Huff sont des valeurs à la compilation qui sont inlinées comme instructions PUSH :

#define constant VALUE_SLOT = 0x00
#define constant OWNER_SLOT = 0x01
#define constant MAX_UINT = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff

Utilisation : [VALUE_SLOT] empile 0x00, [MAX_UINT] empile la valeur complète de 32 octets. Les constantes améliorent la lisibilité sans coûter de gas — elles sont purement syntaxiques.

Quand utiliser Huff

Huff n’est pas un langage généraliste. Utilisez-le quand :

  1. Le gas est la contrainte principale — Contrats MEV où 100 gas détermine la rentabilité.
  2. La taille du bytecode compte — Contrats déployés par d’autres contrats (factories CREATE2) où un initcode plus petit = moins de gas de déploiement.
  3. Vous avez besoin d’un dispatch personnalisé — Tables de saut, sélecteurs compactés, ou encodage ABI non standard.
  4. Vous apprenez l’EVM — Rien n’enseigne mieux l’EVM que l’écriture d’opcodes bruts.

Pour tout le reste, écrivez en Solidity et lisez la sortie du compilateur avec solc --asm. Vous serez plus productif et moins sujet aux erreurs.

Résumé

Huff vous donne une ligne directe vers le bytecode EVM avec juste assez d’abstraction pour rester sain d’esprit. Les macros inlinent le code pour une réutilisation à overhead nul. Les labels gèrent la comptabilité des offsets de saut. Les annotations takes/returns attrapent les erreurs de pile tôt. Dans le prochain article, nous plongerons plus profondément dans la gestion de pile — l’art du dup, swap, et du maintien de votre modèle mental de la pile en synchronisation avec la réalité.