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

Deep EVM #17: Тестирование Huff-контрактов — Foundry, форк-тесты и газ-ассерты

OS
Open Soft Team

Engineering Team

Почему тестирование Huff-контрактов — это особый случай

Huff — это низкоуровневый ассемблерный язык для EVM, дающий прямой контроль над стеком, памятью и хранилищем. Эта мощь имеет свою цену: нет компилятора для перехвата ошибок типов, нет SafeMath, нет автоматической проверки границ. Каждый опкод, который вы пишете, — это именно то, что будет развёрнуто. Это делает тестирование не просто важным, а абсолютно критичным.

В отличие от Solidity, где компилятор генерирует шаблонный код для диспетчеризации функций, раскладки хранилища и ABI-кодирования, в Huff всё это нужно реализовать вручную. Одна ошибочная инструкция SWAP или неверный адрес перехода может привести к утечке средств или навсегда заблокировать контракт.

В этой статье мы построим полную стратегию тестирования Huff-контрактов с помощью Foundry — юнит-тесты, форк-тесты против состояния мейннета, газ-ассерты и дифференциальное тестирование против Solidity-эталона.

Настройка проекта

Для начала настроим Foundry-проект с поддержкой Huff. Вам понадобится компилятор Huff (huffc) наряду с Foundry:

curl -L get.huff.sh | bash
huffup
forge init huff-testing && cd huff-testing
forge install huff-language/foundry-huff

Настройте foundry.toml для использования компилятора Huff:

[profile.default]
src = "src"
out = "out"
libs = ["lib"]
ffi = true

[profile.default.fuzz]
runs = 10000
max_test_rejects = 100000

[profile.default.invariant]
runs = 256
depth = 128

Флаг ffi = true критически важен, потому что foundry-huff использует FFI-вызовы для вызова компилятора Huff во время тестирования.

Написание Huff-контракта для тестирования

Напишем простой ERC20-подобный токен на Huff:

// src/SimpleToken.huff
#define function balanceOf(address) view returns (uint256)
#define function transfer(address, uint256) nonpayable returns (bool)

#define constant BALANCES_SLOT = FREE_STORAGE_POINTER()

#define macro BALANCE_OF() = takes (0) returns (0) {
    0x04 calldataload           // [account]
    BALANCES_SLOT               // [slot, account]
    STORE_ELEMENT_FROM_KEYS(0x00) // [balance_slot]
    sload                       // [balance]
    0x00 mstore
    0x20 0x00 return
}

#define macro TRANSFER() = takes (0) returns (0) {
    0x24 calldataload           // [amount]
    0x04 calldataload           // [to, amount]
    caller                      // [from, to, amount]
    dup1 BALANCES_SLOT
    STORE_ELEMENT_FROM_KEYS(0x00)
    sload                       // [from_bal, from, to, amount]
    dup1 dup5 gt fail jumpi
    dup4 swap1 sub              // [new_from_bal, from, to, amount]
    dup2 BALANCES_SLOT
    STORE_ELEMENT_FROM_KEYS(0x00)
    sstore                      // [from, to, amount]
    swap1 dup1 BALANCES_SLOT
    STORE_ELEMENT_FROM_KEYS(0x00)
    dup1 sload dup4 add
    swap1 sstore pop pop
    0x01 0x00 mstore
    0x20 0x00 return
    fail: 0x00 0x00 revert
}

Настройка тестов в Foundry для Huff

Ключ к тестированию Huff в Foundry — использование библиотеки HuffDeployer из foundry-huff:

// test/SimpleToken.t.sol
pragma solidity ^0.8.19;

import "forge-std/Test.sol";
import "foundry-huff/HuffDeployer.sol";

interface ISimpleToken {
    function balanceOf(address) external view returns (uint256);
    function transfer(address, uint256) external returns (bool);
}

contract SimpleTokenTest is Test {
    ISimpleToken token;
    address alice = makeAddr("alice");
    address bob = makeAddr("bob");

    function setUp() public {
        address deployed = HuffDeployer.deploy("SimpleToken");
        token = ISimpleToken(deployed);
        bytes32 slot = keccak256(abi.encode(alice, uint256(0)));
        vm.store(address(token), slot, bytes32(uint256(1000e18)));
    }

    function test_balanceOf() public view {
        assertEq(token.balanceOf(alice), 1000e18);
        assertEq(token.balanceOf(bob), 0);
    }

    function test_transfer() public {
        vm.prank(alice);
        token.transfer(bob, 100e18);
        assertEq(token.balanceOf(alice), 900e18);
        assertEq(token.balanceOf(bob), 100e18);
    }
}

Газ-снепшоты и регрессионное тестирование

Эффективность по газу — главная причина писать на Huff. Функция газ-снепшотов Foundry позволяет отслеживать потребление газа между запусками тестов:

forge snapshot --match-contract SimpleTokenTest

Это создаёт файл .gas-snapshot:

SimpleTokenTest:test_balanceOf() (gas: 5421)
SimpleTokenTest:test_transfer() (gas: 28934)

Для CI можно установить порог допуска:

forge snapshot --check .gas-snapshot --tolerance 1

Это провалит сборку, если потребление газа какого-либо теста увеличится более чем на 1%. Коммитьте файл .gas-snapshot в репозиторий, чтобы каждый PR проверялся относительно текущего базового уровня.

Дифференциальное тестирование: Huff vs Solidity

Самая мощная техника тестирования Huff — дифференциальное тестирование. Напишите Solidity-реализацию, которая заведомо корректна, и проверьте, что Huff-контракт выдаёт идентичные результаты для всех входных данных:

contract DifferentialTest is Test {
    ISimpleToken huffToken;
    SolidityToken solToken;

    function setUp() public {
        huffToken = ISimpleToken(HuffDeployer.deploy("SimpleToken"));
        solToken = new SolidityToken();
    }

    function testFuzz_balanceOf_differential(
        address account, uint256 balance
    ) public {
        bytes32 huffSlot = keccak256(abi.encode(account, uint256(0)));
        vm.store(address(huffToken), huffSlot, bytes32(balance));
        deal(address(solToken), account, balance);
        assertEq(
            huffToken.balanceOf(account),
            solToken.balanceOf(account),
            "balanceOf mismatch"
        );
    }
}

С fuzz.runs = 10000 Foundry генерирует 10 000 случайных входных данных и проверяет, что обе реализации совпадают.

Форк-тестирование против состояния мейннета

Форк-тестирование позволяет проверить ваш Huff-контракт на реальном состоянии мейннета — бесценно для контрактов, взаимодействующих с существующими протоколами:

forge test --fork-url https://eth-mainnet.g.alchemy.com/v2/KEY \
  --match-test testFork -vvv

Форк-тесты медленнее (сетевые вызовы), но выявляют интеграционные проблемы, которые юнит-тесты пропускают.

Сравнение газа: Huff vs Solidity vs Yul

ОперацияSolidityYulHuff
balanceOf2 6042 3412 187
transfer29 41227 89126 534
Deploy198 234143 89298 421

Huff обычно экономит 10-15% газа по сравнению с Yul и 20-30% по сравнению с Solidity на горячих путях. Для MEV-ботов и высокочастотных операций эта экономия складывается в значительное конкурентное преимущество.

Заключение

Тестирование Huff-контрактов требует дисциплинированного, многоуровневого подхода: юнит-тесты для базовой корректности, фазз-тесты для граничных случаев, дифференциальные тесты против Solidity-эталонов, форк-тесты для реальных интеграций и газ-снепшоты для регрессий производительности. Foundry предоставляет все эти возможности в одном инструменте.