Тестирование смарт-контрактов: unit, fuzz, инварианты и статический анализ
Как проверяют контракты с деньгами: юнит-тесты, фаззинг, инварианты платёжеспособности, «злые» моки токенов, Slither и Mythril на примере эскроу с Foundry.
В этой статье
Контракт с деньгами нельзя «выпустить и починить потом»: после развёртывания код нельзя исправить, а ошибка стоит реальных средств. Поэтому на тестирование уходит больше времени, чем на написание кода. Разберём слои проверки на примере нашего эскроу - это инструментарий Foundry, статические анализаторы и методика мышления.
Слой 1. Юнит-тесты
Самый базовый слой - тест на каждый сценарий и каждую ошибку. В Foundry тесты пишутся на Solidity. Ниже - упрощённые примеры в духе наших тестов:
function test_ConfirmDelivery_PaysSellerMinusFees() public {
uint256 id = _createAndAccept(1_000e6); // сделка на 1000 USDT
vm.prank(buyer);
escrow.confirmDelivery(id);
// платформа 1% = 10, гарант 1% от 990 = 9.9, продавцу 980.1
assertEq(usdt.balanceOf(seller), 980_100_000);
assertEq(usdt.balanceOf(arbiter), 9_900_000);
}
function test_ConfirmDelivery_RevertsForNonBuyer() public {
uint256 id = _createAndAccept(1_000e6);
vm.prank(seller);
vm.expectRevert(Escrow.NotAuthorized.selector);
escrow.confirmDelivery(id);
}Читайте их как спецификацию: что должно произойти и что не должно. Чем больше «отрицательных» тестов - «вызывающий не тот», «статус не тот», «сумма нулевая», - тем надёжнее контракт.
Слой 2. Фаззинг (fuzz-тесты)
Человек придумывает ограниченное число случаев. Фаззер сам генерирует сотни случайных входных значений и ищет такое, на котором тест упадёт. В Foundry достаточно объявить аргументы у тестовой функции:
function testFuzz_SplitPartsSumToWhole(
uint96 amount, uint16 arbiterBps, uint16 buyerBps
) public view {
arbiterBps = uint16(bound(arbiterBps, 0, 3000)); // допустимые ставки
buyerBps = uint16(bound(buyerBps, 0, 10_000));
(uint256 b, uint256 s, uint256 a, uint256 p) = harness.split(amount, arbiterBps, buyerBps);
assertEq(b + s + a + p, amount); // части всегда равны целому
}Именно фаззинг ловит краевые случаи: суммы в 1 wei, максимальные значения, округления. В нашем проекте каждый такой тест выполняется 512 раз на случайных данных при каждом запуске.
Слой 3. Инварианты
Фаззинг проверяет одну функцию. Но реальные ошибки живут во взаимодействии функций: «создали, приняли, оспорили, отменили, вывели…». Инвариантный тест задаёт фаззеру набор допустимых действий и случайно вызывает их в случайном порядке и с разными участниками, после каждого шага проверяя неизменные свойства системы.
Главный инвариант эскроу - платёжеспособность:
function invariant_ContractIsSolvent() public view {
assertGe(token.balanceOf(address(escrow)), escrow.reserved(address(token)));
}«Баланс контракта никогда не меньше суммы всех его обязательств». Что бы ни делали участники, в каком бы порядке ни вызывали функции, это должно оставаться правдой. Наши инвариантные тесты прогоняют по 256 серий глубиной 128 вызовов - десятки тысяч случайных действий.
Слой 4. «Злые» окружения
Чтобы проверить защиту от нестандартных токенов и получателей, пишутся моки - подставные контракты с враждебным поведением:
- токен без возвращаемого
bool(как USDT); - токен с комиссией при переводе;
- токен с чёрным списком адресов;
- получатель с
receive(), который откатывает приём или сжигает газ; - получатель, повторно входящий в контракт (проверка реентрантности).
Если эскроу пережил все эти моки, значит защита из статьи про нестандартные токены работает не на бумаге.
Слой 5. Регрессии и эксплойты
Когда нашлась ошибка, её сначала воспроизводят тестом, который падает, затем исправляют код, и тест остаётся навсегда. Так ошибка не вернётся. В нашем репозитории есть и тесты на уязвимости первой версии контракта, выполняемые прямо на EVM: они наглядно показывают, что именно было сломано и как это исправлено.
Слой 6. Статический анализ
Анализаторы читают код, не запуская его, и ищут известные шаблоны ошибок.
Slither - быстрый анализатор: находит реентрантность, неинициализированные переменные, опасные вызовы, нарушения порядка действий. Выдаёт много находок, из которых часть - ложные срабатывания. Каждую находку нужно разобрать и обосновать: исправить или объяснить, почему она безопасна.
Mythril - символьное исполнение: вместо конкретных чисел он исследует пути выполнения с «абстрактными» значениями и ищет входы, ломающие код. Медленнее, но находит то, что пропускают другие.
Результаты разбора записаны в репозитории - это документ, по которому видно ход мысли, а не просто чек «прошло».
Слой 7. Газ и проверка байткода
Отдельные тесты измеряют газ ключевых операций, чтобы случайная правка не сделала сервис дороже. А скрипт проверки опкодов гарантирует, что в байткоде нет инструкций новее выбранной версии EVM: иначе контракт не заработает в старых сетях (подробнее в статье про CREATE2).
Что тесты не доказывают
Честно о границах.
- Тесты не доказывают отсутствие ошибок. Они показывают, что проверенные сценарии работают. Непредусмотренные могут скрываться.
- Инструменты ограничены. Статический анализ не понимает бизнес-логику: он не заметит, что гарант получает не ту сумму, если код корректен технически.
- Спецификация может быть ошибочной. Тесты проверяют, что код делает то, что в них записано. Если требования неверны, неверными окажутся и тесты.
Поэтому перед работой с крупными суммами необходим независимый аудит - ревью чужими глазами, - а затем программы вознаграждения за найденные уязвимости (bug bounty). Это отдельные этапы, и никакой набор автоматических проверок их не заменяет.
Как мыслить при проектировании
Хороший контракт начинается с вопроса не «как это написать», а «что должно быть всегда истинно». Из таких утверждений получаются инварианты, из инвариантов - тесты, из тестов - уверенность.
Примеры утверждений для эскроу:
- выплата по сделке происходит ровно один раз;
- сумма выплат всегда равна сумме сделки;
- платформа не может забрать больше накопленных комиссий;
- гарант не может быть участником сделки;
- из состояния «Завершена» нельзя выйти.
Что дальше
Тесты не заменяют независимой проверки: аудиты и баг-баунти. Для практики - руководство по Foundry. Вы прошли всю траекторию: от «что такое блокчейн» до устройства виртуальной машины, кода эскроу и методов проверки. Теперь самое время применить знания на практике: прочитайте инструкции по ролям и создайте свою первую сделку.
Читайте также
- 3 · Под капотом
Foundry для начинающих: компиляция, тесты, фаззинг и локальная сеть
Практическое введение в Foundry: установка, структура проекта, unit-тесты, читкоды vm, фаззинг и инварианты, forge, cast и anvil. С рабочим примером.
6 мин чтения - 3 · Под капотом
Реентрантность, checks-effects-interactions и pull-платежи
Как работает атака повторного входа, взломавшая The DAO, и как от неё защищаться: порядок операций, ReentrancyGuard и отложенные платежи на примере эскроу.
5 мин чтения
Упоминается в статьях
- Что такое смарт-контракт и почему ему можно доверять деньги
- Как проверить смарт-контракт перед использованием: пошаговая инструкция
- ERC-20 с нуля: пишем токен на Solidity и разбираем каждую строку
- Целочисленная математика и округление в смарт-контрактах
- Реентрантность, checks-effects-interactions и pull-платежи
- Нестандартные токены: USDT без return, fee-on-transfer, блэклисты и decimals
- Каталог уязвимостей смарт-контрактов: 12 классических ошибок и защита от них
- Прокси и обновляемые контракты: delegatecall, UUPS и цена гибкости
- Foundry для начинающих: компиляция, тесты, фаззинг и локальная сеть
- Аудиты и баг-баунти: как проверяют смарт-контракты и как читать отчёт
Готовы попробовать?
Создайте первую сделку за пару минут. Деньги блокируются в смарт-контракте и выплачиваются по правилам, которые видны заранее.

