Deep EVM #17: Тестирование Huff-контрактов — Foundry, форк-тесты и газ-ассерты
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
| Операция | Solidity | Yul | Huff |
|---|---|---|---|
balanceOf | 2 604 | 2 341 | 2 187 |
transfer | 29 412 | 27 891 | 26 534 |
| Deploy | 198 234 | 143 892 | 98 421 |
Huff обычно экономит 10-15% газа по сравнению с Yul и 20-30% по сравнению с Solidity на горячих путях. Для MEV-ботов и высокочастотных операций эта экономия складывается в значительное конкурентное преимущество.
Заключение
Тестирование Huff-контрактов требует дисциплинированного, многоуровневого подхода: юнит-тесты для базовой корректности, фазз-тесты для граничных случаев, дифференциальные тесты против Solidity-эталонов, форк-тесты для реальных интеграций и газ-снепшоты для регрессий производительности. Foundry предоставляет все эти возможности в одном инструменте.