Тестирование смарт-контрактов: Foundry, Hardhat, фаззинг, инварианты
Смарт-контракт тестируют в четыре слоя: юнит-тесты на каждую функцию, фаззинг со случайными входными данными, инварианты — свойства, которые должны выполняться после любой последовательности вызовов, — и интеграционные тесты на форке реальной сети. Основной инструмент в 2026 году — Foundry (версия 1.8.7 вышла 29 октября), он пишет тесты на Solidity и умеет всё перечисленное из коробки. Hardhat 3 (версия 3.21) удобен, когда рядом TypeScript-бэкенд и фронтенд, и тоже поддерживает тесты на Solidity.
Почему обычных тестов недостаточно
В веб-приложении ошибку чинят следующим релизом. В контракте без механизма обновления ошибка остаётся навсегда, а ущерб измеряется всеми средствами, которые в нём лежат. Кроме того, контракт вызывают не только ваши пользователи, но и любые другие контракты в любом порядке и с любыми аргументами. Юнит-тесты проверяют сценарии, которые придумал разработчик. Фаззинг и инварианты проверяют сценарии, которые он не придумал.
Foundry или Hardhat
| Критерий | Foundry | Hardhat 3 |
|---|---|---|
| Язык тестов | Solidity | Solidity и TypeScript |
| Скорость | Очень высокая, написан на Rust | Высокая, среда исполнения EDR написана на Rust |
| Фаззинг и инварианты | Встроены, настраиваются в foundry.toml | Solidity-тесты с параметрами; глубину поддержки инвариантов проверяйте в документации текущей версии |
| Форк сети | --fork-url и читы vm.createSelectFork | Встроенный форк в конфигурации сети |
| Скрипты деплоя | forge script на Solidity | Hardhat Ignition и скрипты на TypeScript |
| Когда выбирать | Контракты с деньгами, аудит, фаззинг | Монорепозиторий с фронтендом и бэкендом на TypeScript |
На практике многие команды используют оба: Foundry для контрактов и их тестов, Hardhat или библиотеки вроде viem для интеграционных проверок вместе с интерфейсом. Документация: книга Foundry и документация Hardhat.
Слой 1. Юнит-тесты
Юнит-тест проверяет одну функцию в одном состоянии. В Foundry тест — это контракт, функции которого начинаются с test. Читы (cheatcodes) управляют окружением: vm.prank(alice) делает следующий вызов от имени Алисы, vm.warp(t) переводит время, vm.expectRevert(Vault.NothingToWithdraw.selector) ждёт конкретную ошибку.
- Для каждой функции: успешный сценарий, вызов без прав, вызов с нулём, вызов с максимальным значением.
- Для каждого изменения денег: баланс до и после у всех участников, событие с правильными полями.
- Для ролей: выдача, отзыв, передача, попытка вызова отозванной ролью.
- Для времени: ровно на границе срока, за секунду до и через секунду после.
Слой 2. Фаззинг
Фазз-тест — это тест с параметрами. Foundry сам подставляет в них случайные значения, по умолчанию 256 прогонов на тест, и ищет комбинацию, при которой проверка ломается. Найденный контрпример сохраняется и повторяется при следующих запусках. Чтобы случайные значения были осмысленными, их ограничивают функцией bound: например, amount = bound(amount, 1, 1e30);.
Хороший фазз-тест проверяет свойство, а не конкретную цифру. Пример свойства: после депозита суммы x и немедленного вывода пользователь получает ровно x, а баланс контракта возвращается к исходному. Количество прогонов для важных тестов увеличивают в секции [fuzz] файла foundry.toml, а в CI запускают с большим значением, чем локально.
Слой 3. Инвариантное тестирование
Инвариант — это утверждение, которое должно быть истинным всегда, после любой последовательности вызовов. Foundry генерирует цепочки случайных вызовов функций контракта (по умолчанию 256 прогонов глубиной 500 вызовов) и после каждого шага проверяет функции, начинающиеся с invariant_. Это ближе всего к тому, что делает реальный мир с вашим контрактом.
| Тип контракта | Пример инварианта |
|---|---|
| Токен | Сумма балансов всех держателей равна totalSupply |
| Хранилище депозитов | Баланс контракта не меньше суммы обязательств перед пользователями |
| Вестинг | Выданное плюс невыданное равно изначально заблокированному |
| Стейкинг | Сумма начисленных наград не превышает бюджета наград |
| Роли | У роли администратора всегда ровно один держатель |
Для инвариантов пишут обработчик (handler) — промежуточный контракт, который вызывает функции с правдоподобными аргументами и ведёт собственный учёт ожидаемых значений. Без обработчика фаззер тратит большинство вызовов на заведомо отклонённые транзакции. Учтите, что в секции [invariant] параметр fail_on_revert по умолчанию выключен: откаты не считаются ошибкой, и это стоит осознанно настроить.
Слой 4. Тесты на форке сети
Если контракт работает с внешними протоколами — стейблкоинами, оракулами, DEX, — тестируйте его на форке основной сети. Foundry скачивает нужное состояние по RPC и выполняет тест против реальных контрактов. Так вы проверите, что токен действительно возвращает bool, что у оракула правильное число знаков, что адреса в конфигурации верные. Блок форка фиксируйте, иначе тесты станут нестабильными.
Статический анализ и покрытие
- Slither от Trail of Bits находит типовые проблемы: повторный вход, неинициализированные переменные, опасные
delegatecall, затенение переменных. forge coverageпоказывает, какие строки и ветки не выполнялись в тестах. Покрытие — это нижняя граница: 100% строк не значит, что проверены все комбинации.forge snapshotфиксирует расход газа и показывает регрессии при изменениях.- Отдельные фаззеры вроде Echidna и Medusa используют для глубокого поиска нарушений инвариантов.
Минимальный набор для простого токена
Учебный пример. Даже для токена с фиксированной эмиссией мы бы ожидали около десятка тестов: эмиссия ушла на адрес казначейства; перевод меняет оба баланса и эмитит Transfer; перевод сверх остатка откатывается; approve и transferFrom уменьшают разрешение; бесконечное разрешение не уменьшается; permit с правильной подписью работает, а с истёкшим сроком, чужим nonce или в другой сети отклоняется. Плюс один фазз-тест на перевод случайной суммы и один инвариант на равенство суммы балансов и totalSupply.
Как организовать тесты в проекте
- Одна папка на контракт, отдельные файлы для юнит-тестов, фаззинга и инвариантов.
- Общий базовый контракт с развёртыванием и тестовыми адресами, чтобы тесты не дублировали подготовку.
- CI запускает
forge build,forge test, Slither и проверку раскладки storage на каждый pull request. - Для важных веток увеличено число прогонов фаззинга и инвариантов.
- Каждый найденный баг сначала превращается в падающий тест, потом исправляется.
Что требовать от подрядчика
| Вопрос | Хороший признак |
|---|---|
| Есть ли инварианты и какие? | Список инвариантов в документации, связанный с требованиями |
| Сколько прогонов фаззинга в CI? | Явное число, больше значения по умолчанию |
| Тестируется ли интеграция на форке? | Да, с фиксированным блоком |
| Какое покрытие веток? | Отчёт forge coverage с объяснением непокрытых мест |
| Запускается ли статический анализ? | Slither в CI, разобранные предупреждения |
| Получу ли я тесты вместе с кодом? | Да, в репозитории, с инструкцией запуска |
Тесты — это то, что аудитор попросит первым, и от их качества зависит срок аудита. Подробно о подготовке — в чек-листе перед аудитом. Если у вас есть контракт без тестов, начните с инвариантов на движение денег: это даёт больше всего уверенности на единицу времени. Помочь с тестами и аудитом может команда разработки смарт-контрактов и аудита смарт-контрактов. Как устроено тестирование обычных веб-проектов, мы рассказывали в статье о нагрузочном тестировании сайта.
Вопросы и ответы
Что лучше для тестирования смарт-контрактов: Foundry или Hardhat?
Для контрактов с деньгами и глубокого тестирования обычно берут Foundry: встроенный фаззинг, инварианты и высокая скорость. Hardhat удобен, если весь проект на TypeScript, и его третья версия тоже поддерживает тесты на Solidity.
Что такое фаззинг смарт-контракта?
Это запуск теста со случайными входными данными сотни и тысячи раз. Инструмент ищет значения, при которых проверка ломается, и сохраняет найденный контрпример.
Чем инвариантный тест отличается от фазз-теста?
Фазз-тест проверяет одну функцию со случайными аргументами. Инвариантный тест вызывает случайные последовательности функций и после каждого шага проверяет, что ключевые свойства системы не нарушены.
Какое покрытие тестами нужно для смарт-контракта?
Стремятся к полному покрытию строк и веток в коде, где двигаются деньги и права. Но покрытие не заменяет инварианты и фаззинг: оно показывает, что код выполнялся, а не что он верен.
Читайте также
Смарт-контракт простыми словами: что это и когда бизнесу он не нужен
БлокчейнСколько стоит разработка смарт-контракта и из чего складывается цена
БлокчейнАпгрейдируемые смарт-контракты: proxy, UUPS, плюсы и риски
БлокчейнЧек-лист перед аудитом смарт-контракта: что подготовить команде
БлокчейнДеплой и верификация смарт-контракта: ключи, скрипты, Etherscan
БлокчейнСколько стоит аудит смарт-контракта и что в него входит