Мы часто видим проекты, которые уходят на внешний аудит с 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-обёртки |
Как мы пишем тесты: пошаговый план
- Анализ контракта: выделяем инварианты, критические функции и граничные случаи.
- Написание инвариантных тестов: проверяем, что базовые утверждения не нарушаются.
- Unit-тесты для каждого публичного метода: покрываем happy path, граничные случаи и реверты.
- Fuzz-тесты для входных параметров: расширяем покрытие случайными значениями.
- Запуск
forge coverageи анализ: добиваемся branch coverage 85%+. - Интеграция в 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 и инфраструктуры. Гарантируем, что ваш контракт получит покрытие, достаточное для прохождения аудита. Свяжитесь с нами для бесплатной оценки вашего проекта — мы подберём оптимальный объём тестов.







