Перейти к основному содержимому
БлокчейнMar 28, 2026

Deep EVM #9: Язык Huff — макросы, метки и сырые опкоды

OS
Open Soft Team

Engineering Team

Зачем нужен Huff

Solidity — замечательная абстракция, пока вы не упрётесь в её ограничения. Когда контракт должен уместиться в 100 байт рантайм-байткода, диспатчить функции за O(1) через упакованную jump-таблицу или сэкономить 200 газа на горячем пути, который исполняется миллионы раз в сутки, вам нужно нечто ближе к железу. Это нечто — Huff.

Huff — низкоуровневый язык ассемблера EVM с тонкой макросистемой поверх. Нет переменных, нет типов, нет компилятора, который оптимизирует за вашей спиной. Что вы написали — то и окажется on-chain, опкод за опкодом.

Установка Huff

Каноничный компилятор — huffc, написанный на Rust:

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

Команда устанавливает huffc в ~/.huff/bin. Добавьте директорию в PATH и проверьте:

$ huffc --version
huffc 0.3.2

Вы также можете использовать Huff внутри Foundry-проектов с помощью foundry-huff, который позволяет деплоить .huff файлы так же, как .sol.

Hello World: минимальный контракт

Напишем контракт, возвращающий 32-байтное слово 0x01 на любой вызов:

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

Компиляция:

huffc src/HelloWorld.huff -r

Флаг -r выводит рантайм-байткод. Вы увидите что-то вроде 600160005260206000f3 — 10 байт. Контракт на Solidity, возвращающий 1, компилируется в ~200+ байт рантайм-байткода, потому что solc генерирует полный диспатчер функций, хеш метаданных, инициализацию free memory pointer и ABI-кодировщик.

Макросы vs функции

Huff имеет два примитива повторного использования: макросы и функции.

Макросы (#define macro)

Макросы инлайнятся в каждом месте вызова. Нет оверхеда JUMP, нет лишнего газа — компилятор буквально копирует опкоды в вызывающий код. Это выбор по умолчанию для критичного по газу кода.

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

Функции (#define fn)

Функции генерируют реальную пару JUMP/JUMPDEST. Они экономят размер байткода ценой ~22 лишних газа за вызов (8 за JUMP + 1 за JUMPDEST + манипуляции со стеком). Используйте только когда размер байткода важнее газа.

#define fn safe_add() = takes(2) returns(1) {
    // takes: [a, b]
    dup2 dup2       // [a, b, a, b]
    add             // [sum, a, b]
    dup1            // [sum, sum, a, b]
    swap2           // [a, sum, sum, b]
    gt              // [overflow?, sum, b]
    overflow jumpi
    swap1 pop       // [sum]
    back jump
    overflow:
        0x00 0x00 revert
    back:
}

Метки и точки перехода

Метки в Huff — именованные JUMPDEST-локации. Компилятор разрешает их в конкретные смещения байткода во время компиляции.

#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]
}

Каждая метка компилируется в один байт JUMPDEST (0x5b). Ссылки (loop jump, done jumpi) компилируются в PUSH2 <offset> JUMP (или JUMPI). Это в точности то, что вы бы написали вручную на сыром ассемблере EVM — Huff просто берёт на себя учёт смещений.

takes() и returns()

Аннотации takes(n) и returns(m) на макросах и функциях — документация и подсказки компилятору. Они сообщают читателю (и стековому чекеру huffc), сколько элементов стека блок ожидает потребить и произвести.

#define macro ADD_TWO() = takes(2) returns(1) {
    add  // потребляет 2 элемента, производит 1
}

Если реальное поведение стека не совпадает с аннотацией, huffc выдаст предупреждение. Рассматривайте аннотации как бедную систему типов — они предотвращают случайное оставление мусора на стеке или underflow.

Сравнение: Huff vs Solidity байткод

Рассмотрим простую view-функцию getValue(), возвращающую слот хранилища:

Solidity:

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

Solc генерирует ~40 байт для диспатчера + ABI-кодирования.

Huff-эквивалент:

#define function getValue() view returns (uint256)

#define macro GET_VALUE() = takes(0) returns(0) {
    [VALUE_SLOT]    // [slot]
    sload           // [value]
    0x00 mstore     // []  — store in memory
    0x20 0x00 return
}

Huff-версия — 12 байт байткода для тела. Нет оверхеда ABI-кодирования, нет free memory pointer, нет хеша метаданных. Когда вы контролируете вызывающий код (например, MEV-бот вызывает собственный контракт), вы можете убрать всё, что solc считает необходимым.

Константы и слоты хранилища

Константы Huff — значения, вычисляемые во время компиляции, которые инлайнятся как PUSH-инструкции:

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

Использование: [VALUE_SLOT] кладёт 0x00, [MAX_UINT] — полное 32-байтное значение. Константы улучшают читаемость без затрат газа.

Инклюды и структура проекта

Реальные Huff-проекты разносят код по нескольким файлам:

// src/Main.huff
#include "./utils/SafeMath.huff"
#include "./interfaces/IERC20.huff"
#include "./Dispatcher.huff"

#define macro MAIN() = takes(0) returns(0) {
    DISPATCHER()
}

Система инклюдов — простое текстовое включение, без модульной области видимости или пространств имён. Тщательно именуйте макросы во избежание коллизий.

Когда использовать Huff

Huff — не язык общего назначения. Используйте его когда:

  1. Газ — главное ограничение — MEV-контракты, где 100 газа определяют прибыльность.
  2. Размер байткода критичен — контракты, деплоимые другими контрактами (CREATE2-фабрики).
  3. Нужен кастомный диспатч — jump-таблицы, бит-упакованные селекторы, нестандартное ABI.
  4. Вы изучаете EVM — ничто не учит EVM лучше, чем написание сырых опкодов.

Для всего остального пишите на Solidity и читайте вывод компилятора через solc --asm.

Итоги

Huff даёт прямой доступ к байткоду EVM с минимальной абстракцией. Макросы инлайнят код с нулевым оверхедом. Метки берут на себя учёт смещений. Аннотации takes/returns ловят стековые ошибки на ранних этапах. В следующей статье мы глубже погрузимся в управление стеком — искусство dup, swap и синхронизации ментальной модели стека с реальностью.