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

Deep EVM #12: Продвинутый Huff — адаптивное исполнение и on-chain вычисления

OS
Open Soft Team

Engineering Team

За пределами основ

В предыдущих статьях мы освоили фундамент Huff: макросы, метки, стековое управление и jump-таблицы. Теперь пора перейти к продвинутым паттернам, которые отличают учебные примеры от production-контрактов.

Адаптивное исполнение

Адаптивное исполнение — паттерн, при котором контракт меняет свою логику в зависимости от состояния on-chain. Вместо статических ветвлений контракт читает стейт и динамически выбирает путь исполнения.

Пример: переключение режимов

#define constant MODE_SLOT = 0x00
#define constant MODE_NORMAL = 0x00
#define constant MODE_EMERGENCY = 0x01

#define macro ADAPTIVE_DISPATCH() = takes(0) returns(0) {
    [MODE_SLOT] sload       // [mode]
    dup1                    // [mode, mode]
    [MODE_EMERGENCY] eq     // [is_emergency?, mode]
    emergency_path jumpi    // [mode]
    pop                     // []
    NORMAL_LOGIC()
    stop
    emergency_path:
    pop                     // []
    EMERGENCY_LOGIC()
    stop
}

Этот паттерн расходует всего 2100 + 3 + 3 + 3 + 8 = 2117 газа на SLOAD + сравнение + переход. Для MEV-ботов это позволяет переключать стратегии без передеплоя контракта.

Динамический диспатч через storage

Более мощный вариант — хранить адреса переходов в storage и загружать их в рантайме:

#define macro DYNAMIC_DISPATCH() = takes(1) returns(0) {
    // takes: [function_index]
    // Каждый слот хранит JUMPDEST для соответствующей функции
    sload                   // [jump_dest]
    jump                    // GOTO stored destination
}

Это позволяет обновлять маршрутизацию без передеплоя, но добавляет 2100 газа за cold SLOAD (100 за warm).

On-chain вычисления

Некоторые MEV-стратегии требуют вычислений прямо on-chain — например, расчёт оптимального размера свапа по формуле AMM.

Формула Uniswap V2

Оптимальный входной размер для арбитража между двумя пулами:

amountIn = sqrt(reserveA * reserveB * fee) - reserveA

Реализация целочисленного квадратного корня (метод Ньютона) на Huff:

#define macro SQRT() = takes(1) returns(1) {
    // takes: [x]
    // Метод Ньютона: guess = (guess + x / guess) / 2
    dup1                    // [x, x]
    0x02 div                // [guess = x/2, x]
    
    // Итерация 1
    dup2 dup2               // [guess, x, guess, x]
    swap1 div               // [x/guess, guess, x]
    add                     // [guess + x/guess, x]
    0x02 div                // [new_guess, x]
    
    // Итерация 2
    dup2 dup2
    swap1 div
    add
    0x02 div
    
    // Итерация 3
    dup2 dup2
    swap1 div
    add
    0x02 div
    
    // Итерация 4
    dup2 dup2
    swap1 div
    add
    0x02 div
    
    // Итерация 5
    dup2 dup2
    swap1 div
    add
    0x02 div
    
    // Итерация 6
    dup2 dup2
    swap1 div
    add
    0x02 div
    
    // Итерация 7 (достаточно для uint256)
    dup2 dup2
    swap1 div
    add
    0x02 div
    
    swap1 pop               // [result]
}

Семь итераций Ньютона достаточно для сходимости на полном диапазоне uint256. Стоимость: ~7 * (DUP + DUP + SWAP + DIV + ADD + DIV) = ~7 * 28 = ~196 газа. Библиотека на Solidity (OpenZeppelin Math.sqrt) стоит ~300 газа из-за дополнительных проверок и ABI-оверхеда.

Упаковка данных в память

Для минимизации операций с памятью Huff-контракты упаковывают несколько значений в одно 32-байтное слово:

#define macro PACK_TWO_ADDRESSES() = takes(2) returns(1) {
    // takes: [addr1, addr2]
    // Упаковываем два 20-байтных адреса в одно 32-байтное слово
    // addr1 в верхних 20 байтах, addr2 в нижних 20 байтах
    // (теряем 12 байт — допустимо если адреса < 20 байт не используются)
    swap1                   // [addr2, addr1]
    0x60 shl                // [addr2 << 96, addr1]
    or                      // [packed]
}

#define macro UNPACK_FIRST() = takes(1) returns(1) {
    // takes: [packed]
    0x60 shr                // [addr1]
}

#define macro UNPACK_SECOND() = takes(1) returns(1) {
    // takes: [packed]
    0x60 shl 0x60 shr       // [addr2] — очищаем верхние биты
}

Паттерн «один вызов — множество действий»

MEV-боты часто выполняют несколько свапов за один вызов. Вместо множества внешних вызовов контракт декодирует упакованный calldata с массивом действий:

#define macro MULTI_SWAP() = takes(0) returns(0) {
    // Формат calldata:
    // [1 byte: action_count] [32 bytes per action: packed_swap_data]
    
    0x00 calldataload       // [first_word]
    0xf8 shr                // [action_count]
    
    0x01                    // [offset = 1, count]
    
    loop:
        dup2                // [count, offset, count]
        iszero done jumpi   // [offset, count]
        
        // Загружаем swap_data
        dup1 calldataload   // [swap_data, offset, count]
        EXECUTE_SWAP()      // [offset, count]
        
        // Следующее действие
        0x20 add            // [offset + 32, count]
        swap1 0x01 sub swap1 // [offset, count - 1]
        loop jump
    
    done:
        pop pop             // []
        stop
}

Минимизация external calls

Внешние вызовы (CALL, STATICCALL, DELEGATECALL) — самые дорогие операции: 2600 газа за cold call, 100 за warm. Оптимизации:

  1. Batch calls — группируйте множество вызовов в один контракт.
  2. Прямое чтение storage — если знаете layout пула, используйте SLOAD по адресу пула напрямую.
  3. Минимальный calldata — не используйте ABI-кодирование если контролируете обе стороны.
#define macro DIRECT_POOL_READ() = takes(1) returns(2) {
    // takes: [pool_address]
    // Читаем reserve0 и reserve1 через staticcall
    // Селектор getReserves(): 0x0902f1ac
    0x0902f1ac 0xe0 shl     // [selector_padded, pool]
    0x00 mstore             // [pool]
    
    // staticcall(gas, addr, argOffset, argSize, retOffset, retSize)
    0x40                    // retSize = 64
    0x00                    // retOffset = 0
    0x04                    // argSize = 4
    0x00                    // argOffset = 0
    dup5                    // [pool, 0, 4, 0, 64, pool]
    gas                     // [gas, pool, 0, 4, 0, 64, pool]
    staticcall              // [success, pool]
    pop                     // [pool]
    pop                     // []
    
    0x00 mload              // [reserve0]
    0x20 mload              // [reserve1, reserve0]
}

Безопасность продвинутого Huff

С мощью приходит ответственность:

  1. Reentrancy — без модификаторов Solidity вы должны вручную реализовать mutex через storage slot.
  2. Overflow — без SafeMath вы отвечаете за проверки переполнения.
  3. Access control — без onlyOwner вы вручную проверяете caller через CALLER опкод.
#define constant OWNER_SLOT = 0x01
#define constant LOCKED_SLOT = 0x02

#define macro ONLY_OWNER() = takes(0) returns(0) {
    caller                  // [msg.sender]
    [OWNER_SLOT] sload      // [owner, msg.sender]
    eq                      // [is_owner?]
    authorized jumpi
    0x00 0x00 revert
    authorized:
}

#define macro REENTRANCY_GUARD_START() = takes(0) returns(0) {
    [LOCKED_SLOT] sload     // [locked?]
    iszero                  // [not_locked?]
    not_locked jumpi
    0x00 0x00 revert
    not_locked:
    0x01 [LOCKED_SLOT] sstore // lock
}

#define macro REENTRANCY_GUARD_END() = takes(0) returns(0) {
    0x00 [LOCKED_SLOT] sstore // unlock
}

Тестирование Huff-контрактов

Используйте Foundry для тестирования:

// test/Contract.t.sol
import "foundry-huff/HuffDeployer.sol";

contract ContractTest is Test {
    address contractAddr;
    
    function setUp() public {
        contractAddr = HuffDeployer.deploy("Contract");
    }
    
    function testSwap() public {
        (bool success, bytes memory data) = contractAddr.call(
            abi.encodePacked(uint8(0x00), uint256(1000))
        );
        assertTrue(success);
    }
}

Итоги

Продвинутый Huff открывает возможности, недоступные на Solidity: адаптивное исполнение через storage, on-chain вычисления с минимальным газом, упаковку данных и мульти-операции за один вызов. Но с мощью приходит ответственность — reentrancy guard, overflow checks и access control ложатся полностью на разработчика. В следующей статье мы переходим к MEV — области, где эти навыки приносят реальную прибыль.