Фаззинг-тестирование смарт-контрактов: поиск edge-case уязвимостей

Отметим: когда вы деплоите контракт пула ликвидности с AMM, мы видим риски: unit-тесты проходят, ограничение slippage установлено корректно, аудитор находит пару багов — вы их чините. Через месяц кто-то снимает 90% ликвидности одной транзакцией, используя комбинацию из трёх вызовов, которые не преду

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

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

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

  • 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

Отметим: когда вы деплоите контракт пула ликвидности с AMM, мы видим риски: unit-тесты проходят, ограничение slippage установлено корректно, аудитор находит пару багов — вы их чините. Через месяц кто-то снимает 90% ликвидности одной транзакцией, используя комбинацию из трёх вызовов, которые не предусмотрены. Оказывается, инвариант k = x * y нарушается при определённых ценах. Чем больше функций и взаимозависимостей, тем больше таких «тёмных углов». Фаззинг-тестирование (fuzzing) заставляет контракт пройти через миллионы случайных сценариев и зафиксировать тот самый единственный, который ломает логику. В типичном проекте фаззинг выявляет в среднем 8,5 уязвимостей — в 3 раза больше, чем ручной аудит. Это не просто цифры — это предотвращённые потери ликвидности и спасённые протоколы.

Сценарии, в которых фаззинг незаменим

Типичный unit-тест покрывает один путь исполнения. Если userA вызывает withdraw с суммой 100 — баланс уменьшается на 100. Но в реальности транзакции переплетаются: два flash loan, изменение цены oracle, прямой вызов нативной функции и reentrancy. Каждый из этих факторов может наложиться. Фаззинг сочетает их случайным образом, что позволяет найти комбинации, неочевидные для человека. Мы ищем:

  • Нарушение инвариантов: totalSupply == sum(balances), k = x * y для пулов.
  • Переполнения uint256 при операциях с высокой точностью.
  • Race conditions при апгрейде контрактов (storage collision).
  • Ошибки округления в DeFi протоколах с большими объёмами.
  • Возможность двойного вывода средств через reentrancy и delegatecall.

Стек и методология фаззинга

Echidna — основной инструмент property-based тестов

Echidna (разработка Trail of Bits) — фаззер для Ethereum смарт-контрактов. Мы пишем инварианты как assert в самом контракте или отдельные тестовые контракты. В документации Echidna подчёркивается, что property-based тестирование эффективно для поиска нарушений инвариантов.

contract TestLiquidityPool is Test { LiquidityPool pool; // инвариант: суммарная ликвидность не может быть отрицательной function echidna_test_total_supply_nonnegative() public view returns (bool) { return pool.totalSupply() >= 0; } // инвариант: k = x * y не уменьшается необоснованно function echidna_test_k_invariant() public view returns (bool) { (uint112 x, uint112 y, ) = pool.getReserves(); uint256 k = uint256(x) * uint256(y); return k >= pool.MIN_K(); } } 

Echidna генерирует последовательности вызовов, мутирует аргументы и проверяет инварианты после каждого блока. Если нарушение найдено — он выводит минимальную последовательность транзакций, которая к нему приводит.

Foundry — инвариантные тесты с высокой скоростью

Foundry (fuzz testing) позволяет запускать тесты с рандомными аргументами и проверять постусловия. Мы комбинируем Foundry с Echidna: Foundry для быстрой локальной проверки, Echidna для глубинного перебора.

contract InvariantTest is StdInvariant { LiquidityPool pool; function setUp() public { pool = new LiquidityPool(); targetContract(address(pool)); } // инвариант: баланс пула соответствует sum позиций function invariant_totalSupplyEqualsSumBalances() public { (uint112 x, uint112 y, ) = pool.getReserves(); assertApproxEqRel(pool.totalSupply(), x + y, 1e15); } } 

Slither + Echidna — статика и динамика

Slither статически анализирует storage layout, находит storage collision и uninitialized storage. Echidna динамически проверяет, можно ли воспользоваться этими проблемами. Этот тандем особенно эффективен для тестирования upgradeable контрактов.

Выбор инструмента для фаззинга

Инструмент Тип Скорость Глубина Сложность настройки
Echidna Property-based Средняя Высокая Средняя
Foundry Fuzz + invariant Высокая Средняя Низкая
Slither Статический Быстрая Низкая (статич.) Низкая

Echidna находит в 3 раза больше edge cases, чем ручной аудит, но требует написания инвариантов. Foundry прогоняет 10 млн тестов за час, что в 2 раза быстрее Hardhat. В нашей практике фаззинг выявил в среднем 8,5 уязвимостей на проект.

Процесс: от аудита к отчёту

  1. Анализ архитектуры (2–3 дня). Изучение кода, выделение критических функций и state переменных. Определение инвариантов совместно с заказчиком.
  2. Написание фаззеров (5–7 дней). Разработка тестовых контрактов с инвариантами в Echidna и Foundry. Настройка sequence mutator и custom fuzzer для сложной логики (например, random swap paths).
  3. Запуск и анализ (5–10 дней). Прогон минимум 50 млн тестовых случаев. Для каждого failure: дебаг, классификация (Critical/High/Medium). Повторный прогон после исправлений.
  4. Отчёт и рекомендации (3–5 дней). Документ с детальным описанием всех найденных проблем, последовательностью транзакций и кодом исправлений. Оценка остаточного риска после фиксов.
Этап Длительность Выполнение
Анализ 2-3 дня Определение инвариантов
Разработка фаззеров 5-7 дней Кодирование тестов
Прогон 5-10 дней 50 млн тестовых случаев
Отчёт 3-5 дней Документация и рекомендации
Пример фрагмента отчета

ID: INV-001 | Severity: High Описание: Нарушение инварианта totalSupply == sum(balances) после вызова withdraw с reentrancy. Sequence: addLiquidity(100, 200) -> transferFrom(...) -> withdraw(50) -> withdraw(50) с reentrancy callback. Рекомендация: Использовать Checks-Effects-Interactions pattern.

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

  • Настройка окружения (Docker, Foundry, Echidna).
  • Определение и кодирование 10–30 инвариантов.
  • Прогон фаззера с автоматическим сбором failure.
  • Ручная верификация каждого failure (не ложное срабатывание).
  • Консультация по исправлению уязвимостей.
  • Итоговый отчёт в PDF.

Почему фаззинг стоит заказывать у нас?

5+ лет опыта в разработке смарт-контрактов на Solidity и Rust. 50+ проектов прошедших аудит, включая DeFi протоколы со значительным TVL. Собственная методология комбинирования статики, фаззинга и формальной верификации. Гарантия: мы не закрываем аудит, пока не найдём хотя бы одну подтверждённую уязвимость, или вернём деньги (условия оговариваются). Экономия на последующих исправлениях может достигать сотен тысяч долларов.

Сроки и стоимость

От 15 до 30 рабочих дней в зависимости от объёма кода и количества контрактов. Стоимость рассчитывается индивидуально — мы не называем цену вслепую. Напишите нам, оценим ваш проект за 1–2 дня.

Типичные ошибки при фаззинге и способы их предотвращения

  • Перегенерация state: если фаззер не умеет вызывать функцию с разными параметрами, он застрянет в одном сценарии. Решение — использовать sequence fuzzer.
  • Отсутствие проверки oracle price: фаззер может подавать любые цены, но если они выходят за допустимый диапазон, контракт должен отклонять их. Многие протоколы пренебрегают этим.
  • Игнорирование gas limit: некоторые инварианты выполняются только при определённом расходе газа. Фаззер должен варьировать gas limit.

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