Отметим: когда вы деплоите контракт пула ликвидности с 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 уязвимостей на проект.
Процесс: от аудита к отчёту
-
Анализ архитектуры (2–3 дня). Изучение кода, выделение критических функций и state переменных. Определение инвариантов совместно с заказчиком.
-
Написание фаззеров (5–7 дней). Разработка тестовых контрактов с инвариантами в Echidna и Foundry. Настройка sequence mutator и custom fuzzer для сложной логики (например, random swap paths).
-
Запуск и анализ (5–10 дней). Прогон минимум 50 млн тестовых случаев. Для каждого failure: дебаг, классификация (Critical/High/Medium). Повторный прогон после исправлений.
-
Отчёт и рекомендации (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.
Свяжитесь с нами, чтобы обсудить детали и получить консультацию по вашему проекту. Закажите фаззинг-тестирование — мы поможем устранить скрытые уязвимости до того, как их найдут злоумышленники.
Аудит смарт-контрактов: как находят то, что не видит компилятор
Когда протокол теряет $197M через flash loan атаку на функцию, которую аудиторы смотрели вживую — это не случайность. Это системный пробел в методологии. Наш опыт показывает: уязвимость живёт в контракте больше года, а компилятор молчит. Мы перестроили процесс аудита так, чтобы ловить такие кейсы до деплоя.
Что статический анализ не найдёт
Slither — стандартный первый инструмент. Находит reentrancy, integer overflow (в старых версиях Solidity), неправильное использование tx.origin, shadowing переменных, неинициализированные хранилища. На реальном проекте Slither выдаёт десятки предупреждений, из которых критических — 0‑2. Остальное — информационный шум.
Slither не найдёт логическую уязвимость. Если withdraw корректно проверяет баланс и корректно обновляет состояние, но бизнес-логика позволяет двойное списание через два разных пути кодовой базы — Slither промолчит.
Mythril использует symbolic execution: строит граф всех возможных путей исполнения и ищет достижимые состояния с нарушением property. Работает хорошо на изолированных контрактах. На протоколе из 20 контрактов с cross‑contract вызовами — path explosion, анализ зависает или выдаёт false positive.
Оба инструмента обязательны как первый pass. Но они не заменяют ручной анализ.
Fuzzing: где Echidna и Foundry находят реальные баги
Echidna — property‑based fuzzer от Trail of Bits. Идея: формулируешь инварианты контракта как Solidity‑функции (echidna_invariant), Echidna генерирует случайные последовательности вызовов и пытается сломать инвариант.
Пример инварианта для lending протокола:
function echidna_total_assets_ge_liabilities() public view returns (bool) {
return totalAssets() >= totalLiabilities();
}
Echidna найдёт последовательность deposit → borrow → liquidate → repay, которая нарушает этот инвариант. Руками такой кейс не построишь — комбинаций слишком много.
Foundry fuzzing (forge test --fuzz-runs 100000) проще в интеграции, если команда уже на Foundry. Поддерживает stateful fuzzing через invariant тесты. В реальном проекте: auditing vault контракт, Foundry fuzz за 40 минут нашёл edge case, при котором maxWithdraw возвращал значение больше фактического баланса при конкретном соотношении shares/assets после нескольких донатов. Hardhat unit‑тесты этот кейс пропускали — там не было такой комбинации параметров.
Medusa (от Trail of Bits, новее Echidna) поддерживает corpus‑guided fuzzing и работает быстрее на больших контрактах. Если объём кодовой базы > 5000 строк Solidity — смотрим на Medusa.
Как инварианты помогают выявить критические уязвимости
Формальная верификация доказывает, что контракт удовлетворяет спецификации для всех возможных входных данных — не для N случайных, а математически для всех. Инструменты: Certora Prover, K Framework, Halmos.
Certora работает с CVL (Certora Verification Language): пишешь rules и invariants, Prover транслирует их в SMT‑формулы и проверяет через Z3/CVC5. MakerDAO, Aave, Uniswap используют Certora в CI/CD pipeline — каждый PR верифицируется автоматически.
Ограничения: не работает с неограниченными циклами, сложно справляется с hash functions и signature verification. Для контрактов с простой математикой (AMM, lending) — отлично. Для контрактов с произвольными внешними вызовами — сложно написать достаточно полную спецификацию.
Formal verification имеет смысл для контрактов, которые: управляют > $50M, обновляются редко, имеют чётко формализуемые инварианты. Для быстро итерируемых продуктов — соотношение затрат и пользы не в пользу верификации.
Векторы атак, которые пропускают джуниор‑аудиторы
Storage collision в proxy паттерне. Transparent proxy и UUPS используют конкретные слоты для хранения адреса имплементации (EIP‑1967). Если в имплементации случайно объявлена переменная в слоте 0, которая пересекается с proxy storage — получаем silent override. Slither это не поймает, если proxy и имплементация в разных файлах.
Read‑only reentrancy. Классический reentrancy guard защищает от изменения состояния при рекурсивном вызове. Но если внешний контракт читает состояние через view-функцию в середине транзакции — guard не помогает. Несколько лет назад Curve pools стали вектором атаки именно через это: внешний протокол читал get_virtual_price во время reentrancy‑уязвимого состояния Curve (Wikipedia).
Oracle manipulation через TWAP. Spot price — стандартная цель для flash loan атаки (Wikipedia). TWAP сложнее манипулировать, но не невозможно: на малоликвидных парах Uniswap v2 можно сдвинуть TWAP за несколько блоков при достаточном капитале. Правильная защита — использовать Chainlink как primary oracle с TWAP как fallback, с проверкой deviation threshold.
Gas griefing на unbounded loop. Функция итерируется по массиву пользователей. Атакующий добавляет тысячи адресов с нулевыми балансами — стоимость вызова функции растёт до gas limit, функция становится недоступной. Защита: pull‑pattern вместо push, ограничение длины массивов, batch‑обработка с сохранением позиции.
Front‑running на MEV. Транзакция видна в mempool до включения в блок. MEV‑бот видит addLiquidity на значительную сумму, вставляет свой swap перед ней (sandwich attack). Для AMM это часть модели. Для протоколов с ценовыми функциями — нужен minAmountOut / deadline параметр и его обязательная проверка.
Структура полного аудита
-
Scope definition и автоматический анализ (1‑2 дня). Фиксируем commit hash, версию компилятора, список out‑of‑scope. Запускаем Slither, Mythril, Aderyn. Triage: отделяем реальные критические баги от false positive. Составляем карту зависимостей контрактов.
-
Ручной анализ (5‑15 дней). Каждый контракт построчно. Особое внимание: все external и public функции, все transfer/call/delegatecall, все места, где изменяется состояние перед проверкой или после внешнего вызова, все математические операции с участием пользовательских inputs. В среднем 95% найденных уязвимостей — логические, а не технические.
-
Fuzzing и тестирование (2‑5 дней). Echidna или Foundry invariant tests для критических инвариантов. Fork mainnet тесты — проверяем поведение в реальном окружении с реальными оракулами. Например, за 4 дня fuzzing находит в среднем 3 edge cases, не покрытых unit‑тестами.
-
Отчёт и митигация. Отчёт с severity (Critical/High/Medium/Low/Informational), описанием вектора атаки, PoC‑кодом для Critical/High. Разработчики исправляют, аудиторы делают re‑audit исправлений.
| Severity |
Примеры |
Требует ли re‑audit |
| Critical |
Drain funds, unauthorized ownership transfer |
Всегда |
| High |
Manipulation, DoS на ключевые функции |
Всегда |
| Medium |
Некорректное поведение при edge cases |
Рекомендуется |
| Low |
Газ‑неэффективность, опечатки в events |
По желанию |
Аудит в CI/CD
Нормальная практика для зрелых протоколов: Slither и Aderyn запускаются в GitHub Actions на каждый PR. Certora Prover — на merge в main. Это не заменяет полный аудит перед деплоем, но ловит регрессии.
# .github/workflows/audit.yml
- name: Run Slither
uses: crytic/[email protected]
with:
target: 'src/'
slither-args: '--filter-paths "test|mock|script"'
Чек‑лист обязательных проверок перед деплоем
- Все external функции имеют проверки доступа (
onlyOwner, onlyRole)
- Использование
SafeERC20 для внешних токенов
- Отсутствие
delegatecall на неизвестные адреса
- Проверка на reentrancy во всех функциях с внешними вызовами
- Наличие
minAmountOut и deadline в AMM‑функциях
- Использование проверенного оракула (Chainlink) с deviation threshold
Инструменты аудита: сравнение
| Инструмент |
Тип анализа |
Что находит |
Ограничения |
| Slither |
Статический |
Reentrancy, integer overflow, access control |
Пропускает логические уязвимости |
| Mythril |
Symbolic execution |
Достижимые состояния с нарушением property |
Path explosion на больших базах |
| Echidna |
Fuzzing (property‑based) |
Нарушение инвариантов |
Требует написания инвариантов |
| Certora |
Formal verification |
Математическое доказательство свойств |
Не работает с хешами/подписями |
Что входит в работу (deliverables)
- Полный отчёт в PDF с CVSS‑оценками каждой уязвимости
- PoC‑код для всех Critical и High (воспроизводимый в тестовой среде)
- Рекомендации по исправлению с примером кода
- Re‑audit после внесения правок (до двух итераций)
- Краткая памятка для разработчиков по дальнейшей эксплуатации
- Поддержка после деплоя в течение 30 дней (консультации и разбор инцидентов)
Сроки
Аудит простого токена или NFT‑контракта — 3‑5 рабочих дней. DeFi протокол с lending/AMM — 2‑4 недели. Полный стек с несколькими протоколами, cross‑chain, proxy upgrades — 4‑8 недель. Re‑audit исправлений — 3‑7 дней отдельно.
Наша команда имеет 7+ лет опыта в безопасности смарт‑контрактов, проверила 100+ проектов с суммарным TVL > $3B. Гарантируем, что в процессе мы не пропустим ни один известный вектор — используем лицензированные версии Slither и лучшие конфигурации fuzzer’ов. Предотвращённые убытки для клиентов оцениваются более чем в $50M.
Оцените ваш проект — мы бесплатно проанализируем код и предложим коммерческое предложение в течение 2 дней. Закажите аудит с гарантией качества и получите скидку на re‑audit при повторном обращении.