Перейти к содержимому
Все статьи
Блокчейн

Тестирование смарт-контрактов: Foundry, Hardhat, фаззинг, инварианты

·30 октября 2026 г.· 12 мин чтения
Нужен смарт-контракт или dApp?Разработка смарт-контрактов

Смарт-контракт тестируют в четыре слоя: юнит-тесты на каждую функцию, фаззинг со случайными входными данными, инварианты — свойства, которые должны выполняться после любой последовательности вызовов, — и интеграционные тесты на форке реальной сети. Основной инструмент в 2026 году — Foundry (версия 1.8.7 вышла 29 октября), он пишет тесты на Solidity и умеет всё перечисленное из коробки. Hardhat 3 (версия 3.21) удобен, когда рядом TypeScript-бэкенд и фронтенд, и тоже поддерживает тесты на Solidity.

Почему обычных тестов недостаточно

В веб-приложении ошибку чинят следующим релизом. В контракте без механизма обновления ошибка остаётся навсегда, а ущерб измеряется всеми средствами, которые в нём лежат. Кроме того, контракт вызывают не только ваши пользователи, но и любые другие контракты в любом порядке и с любыми аргументами. Юнит-тесты проверяют сценарии, которые придумал разработчик. Фаззинг и инварианты проверяют сценарии, которые он не придумал.

Foundry или Hardhat

КритерийFoundryHardhat 3
Язык тестовSoliditySolidity и TypeScript
СкоростьОчень высокая, написан на RustВысокая, среда исполнения EDR написана на Rust
Фаззинг и инвариантыВстроены, настраиваются в foundry.tomlSolidity-тесты с параметрами; глубину поддержки инвариантов проверяйте в документации текущей версии
Форк сети--fork-url и читы vm.createSelectForkВстроенный форк в конфигурации сети
Скрипты деплояforge script на SolidityHardhat 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.

Как организовать тесты в проекте

  1. Одна папка на контракт, отдельные файлы для юнит-тестов, фаззинга и инвариантов.
  2. Общий базовый контракт с развёртыванием и тестовыми адресами, чтобы тесты не дублировали подготовку.
  3. CI запускает forge build, forge test, Slither и проверку раскладки storage на каждый pull request.
  4. Для важных веток увеличено число прогонов фаззинга и инвариантов.
  5. Каждый найденный баг сначала превращается в падающий тест, потом исправляется.

Что требовать от подрядчика

ВопросХороший признак
Есть ли инварианты и какие?Список инвариантов в документации, связанный с требованиями
Сколько прогонов фаззинга в CI?Явное число, больше значения по умолчанию
Тестируется ли интеграция на форке?Да, с фиксированным блоком
Какое покрытие веток?Отчёт forge coverage с объяснением непокрытых мест
Запускается ли статический анализ?Slither в CI, разобранные предупреждения
Получу ли я тесты вместе с кодом?Да, в репозитории, с инструкцией запуска

Тесты — это то, что аудитор попросит первым, и от их качества зависит срок аудита. Подробно о подготовке — в чек-листе перед аудитом. Если у вас есть контракт без тестов, начните с инвариантов на движение денег: это даёт больше всего уверенности на единицу времени. Помочь с тестами и аудитом может команда разработки смарт-контрактов и аудита смарт-контрактов. Как устроено тестирование обычных веб-проектов, мы рассказывали в статье о нагрузочном тестировании сайта.

Вопросы и ответы

Что лучше для тестирования смарт-контрактов: Foundry или Hardhat?

Для контрактов с деньгами и глубокого тестирования обычно берут Foundry: встроенный фаззинг, инварианты и высокая скорость. Hardhat удобен, если весь проект на TypeScript, и его третья версия тоже поддерживает тесты на Solidity.

Что такое фаззинг смарт-контракта?

Это запуск теста со случайными входными данными сотни и тысячи раз. Инструмент ищет значения, при которых проверка ломается, и сохраняет найденный контрпример.

Чем инвариантный тест отличается от фазз-теста?

Фазз-тест проверяет одну функцию со случайными аргументами. Инвариантный тест вызывает случайные последовательности функций и после каждого шага проверяет, что ключевые свойства системы не нарушены.

Какое покрытие тестами нужно для смарт-контракта?

Стремятся к полному покрытию строк и веток в коде, где двигаются деньги и права. Но покрытие не заменяет инварианты и фаззинг: оно показывает, что код выполнялся, а не что он верен.

Нужна помощь с проектом?

Обсудим задачу и предложим решение — от сайта до SaaS и защиты.

Связаться