Юнит-тестирование смарт-контрактов: Foundry, Solidity, полное покрытие

Мы часто видим проекты, которые уходят на внешний аудит с 20% покрытием тестами — и получают отчёт на 40 страниц, где половина ошибок могла быть отловлена обычным тест-сьютом. Наша практика: пишем юнит-тесты параллельно с разработкой контракта на [Solidity](https://en.wikipedia.org/wiki/Solidity), и

Направления блокчейн-разработки

Часто задаваемые вопросы

Последние работы

  • image_website-b2b-advance_0.webp
    Разработка сайта компании B2B ADVANCE
    1441
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1301
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    998
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1267
  • image_logo-advance_0.webp
    Разработка логотипа компании B2B Advance
    713
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    1003

Мы часто видим проекты, которые уходят на внешний аудит с 20% покрытием тестами — и получают отчёт на 40 страниц, где половина ошибок могла быть отловлена обычным тест-сьютом. Наша практика: пишем юнит-тесты параллельно с разработкой контракта на Solidity, используя Foundry. Это ускоряет цикл и снижает стоимость аудита до 40%. Аудит контракта средней сложности стоит десятки тысяч долларов, и наши тесты помогают сократить эту сумму на 30–40%. Закажите консультацию, чтобы оценить ваш проект.

Но покрытие само по себе не цель. 100% line coverage при нулевом branch coverage — иллюзия безопасности. Реальный кейс: токен-контракт с тестами на transfer и mint, но без теста на transfer(address(0), amount). На деплое через три дня — баг с потерей токенов. Строчка покрыта, ветка — нет. Это типичная ошибка при unit-тестировании смарт-контрактов: тестируют только happy path.

Причины вытеснения Hardhat Foundry

Ранее большинство проектов писали тесты на JavaScript через Hardhat + Chai. Это работало. Но Foundry изменил стандарт.

Скорость. Foundry компилирует и запускает тесты нативно через EVM-реализацию на Rust (revm). Тест-сьют на 200 тестов — 4–8 секунд против 45–90 секунд на Hardhat. При TDD это принципиально. Foundry работает в 5–10 раз быстрее Hardhat.

Fuzz-тестирование из коробки. Любая функция с параметрами становится fuzz-тестом:

function testFuzz_transfer(address to, uint256 amount) public { vm.assume(to != address(0)); vm.assume(amount <= token.balanceOf(alice)); uint256 balanceBefore = token.balanceOf(to); vm.prank(alice); token.transfer(to, amount); assertEq(token.balanceOf(to), balanceBefore + amount); } 

Foundry прогоняет этот тест 256 раз (конфигурируется) с разными значениями. В нашей практике fuzz-тесты находили edge cases — переполнение при расчёте наград — которые ручные тесты пропускали. Fuzz-тесты находят на 60% больше багов, чем обычные unit-тесты.

Cheatcodes. vm.prank, vm.warp, vm.roll, vm.deal — манипуляция состоянием EVM прямо в тестах на Solidity. Например, vm.prank(alice) устанавливает caller на alice для следующего вызова. Это даёт полный контроль над EVM в тестах.

Сравните: Hardhat требует писать тесты на JS/TS, оборачивать вызовы в промисы, подключать плагины для fuzz. Foundry предлагает всё из коробки на Solidity — меньше кода, выше скорость.

Архитектура тест-сьюта

Что тестировать в первую очередь

Не начинаем с happy path. Начинаем с инвариантов: что никогда не должно нарушаться независимо от порядка вызовов.

Для ERC-20 токена инварианты: totalSupply == sum(balances), balanceOf(address(0)) == 0, allowance после approve == указанное значение. Для стейкинг-контракта: totalStaked == sum(userStakes), rewards(user) >= 0.

Invariant-тесты в Foundry (forge test --match-test invariant) запускают последовательности случайных вызовов и проверяют, что инварианты выдерживаются. Это мощнее unit-тестов: находит нарушения, которые возникают только при определённой последовательности транзакций.

Пример: тестирование AMM пула

Для пула ликвидности с функцией swap мы пишем инвариант: произведение резервов (x * y) должно оставаться постоянным после swap с учётом комиссии. Затем fuzz-тесты генерируют случайные объёмы swap и проверяют, что инвариант выполняется. Если в контракте есть баг с округлением, fuzz найдёт его за несколько секунд.

Структура тест-файла

contract TokenTest is Test { Token token; address alice = makeAddr("alice"); address bob = makeAddr("bob"); function setUp() public { token = new Token("Test", "TST", 1_000_000e18); deal(address(token), alice, 1000e18); } // Юнит: конкретный сценарий function test_transfer_reducesBalance() public { vm.prank(alice); token.transfer(bob, 100e18); assertEq(token.balanceOf(alice), 900e18); assertEq(token.balanceOf(bob), 100e18); } // Граничный случай function test_transfer_revertsOnInsufficientBalance() public { vm.prank(alice); vm.expectRevert(); token.transfer(bob, 1001e18); } // Fuzz function testFuzz_transfer(uint256 amount) public { amount = bound(amount, 0, 1000e18); vm.prank(alice); token.transfer(bob, amount); assertEq(token.balanceOf(alice) + token.balanceOf(bob), 1000e18); } } 

Что такое branch coverage и почему он важнее line coverage?

forge coverage выдаёт line, branch, statement и function coverage. Нас интересует прежде всего branch coverage: каждое условие должно быть проверено в обоих состояниях.

Реальные цели по покрытию:

Тип контракта Line coverage Branch coverage
Критичные (vault, bridge) 95%+ 85%+
DeFi (lending, AMM) 90%+ 80%+
Вспомогательные (utils, helpers) 80%+ 70%+
View-only контракты 75%+ 60%+

Стопроцентного покрытия для Solidity добиться сложно — некоторые ветки для защитных проверок требуют нарушения инвариантов EVM, что в тесте невозможно. Но 85% branch coverage — достижимо и достаточно для аудита.

Сравнение Foundry vs Hardhat

Критерий Foundry Hardhat
Язык тестов Solidity JavaScript/TypeScript
Fuzz-тесты Встроены Через плагин
Скорость на 200 тестов 4-8 сек 45-90 сек
Cheatcodes Нативный Solidity JS-обёртки

Как мы пишем тесты: пошаговый план

  1. Анализ контракта: выделяем инварианты, критические функции и граничные случаи.
  2. Написание инвариантных тестов: проверяем, что базовые утверждения не нарушаются.
  3. Unit-тесты для каждого публичного метода: покрываем happy path, граничные случаи и реверты.
  4. Fuzz-тесты для входных параметров: расширяем покрытие случайными значениями.
  5. Запуск forge coverage и анализ: добиваемся branch coverage 85%+.
  6. Интеграция в CI: тесты запускаются при каждом коммите. Используем GitHub Actions с forge test и forge coverage. Результаты отправляются в Pull Request.

Что входит в работу?

  • Полный тест-сьют на Foundry с юнит-тестами, fuzz-тестами и инвариантами.
  • Отчёт о покрытии (line + branch) в формате HTML/PDF.
  • Документация по тестам: описание сценариев, инструкция по запуску.
  • Обучение вашей команды работе с тестами (2-часовой воркшоп).
  • Месячная поддержка после сдачи: фикс тестов при изменениях контракта.

Процесс и сроки

Пишем тесты параллельно с разработкой. Типичный объём: на каждые 100 строк контракта — 150–300 строк тестов. Для контракта сложности 2 (стейкинг, vesting, simple AMM) — 2–3 рабочих дня на полный тест-сьют с фаззингом. Сроки обсуждаются индивидуально — напишите нам, оценим ваш проект бесплатно.

Перед сдачей прогоняем forge test -vvv и forge coverage. Отчёт по покрытию идёт вместе с кодом. Если покрытие упало ниже порогов — выясняем причину до деплоя.

Качественный тест-сьют окупается за счёт сокращения времени аудита на десятки часов, что экономит тысячи долларов. Наши клиенты экономят в среднем 30–40% бюджета на аудите благодаря таким тестам.

За годы работы мы протестировали более 30 смарт-контрактов для DeFi, NFT и инфраструктуры. Гарантируем, что ваш контракт получит покрытие, достаточное для прохождения аудита. Свяжитесь с нами для бесплатной оценки вашего проекта — мы подберём оптимальный объём тестов.