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

Слабые тесты смарт-контрактов приводят к дорогим ошибкам на деплое и раздувают бюджет аудита. Мы разрабатываем юнит-тесты на Solidity с использованием Foundry, обеспечивая полное покрытие ветвлений и инвариантов. Наша команда реализует проект под ключ — от написания тестов до поддержки, помогая вам выпустить надёжный продукт без срыва сроков.

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

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

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

  • Разработка сайта компании B2B ADVANCE
    Разработка сайта компании B2B ADVANCE
    1530
  • Разработка веб-приложения для компании FEEDME
    Разработка веб-приложения для компании FEEDME
    1354
  • Разработка веб-сайта для компании БЕЛФИНГРУПП
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    1070
  • Разработка интернет магазина для компании FURNORO
    Разработка интернет магазина для компании FURNORO
    1328
  • Разработка логотипа компании B2B Advance
    Разработка логотипа компании B2B Advance
    772
  • Разработка веб-приложения для компании Enviok
    Разработка веб-приложения для компании Enviok
    1074

Мы часто видим проекты, которые уходят на внешний аудит с 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 и инфраструктуры. Гарантируем, что ваш контракт получит покрытие, достаточное для прохождения аудита. Свяжитесь с нами для оценки вашего проекта — мы подберём оптимальный объём тестов.