Тестирование: юнит, форк-тесты и инварианты
Тест проверяет «делает ли код то, что я написал»; инвариант — «остаётся ли система честной всегда».
Инвариант — свойство системы, которое должно выполняться всегда, при любой последовательности допустимых действий (например, «сумма балансов = общему предложению»).
Три уровня тестов
Тестирование DeFi многослойно, и каждый слой ловит свой класс ошибок:
- Юнит-тесты — проверяют конкретные функции на конкретных входах: «депозит увеличивает баланс», «вывод чужого падает». Основа основ.
- Форк-тесты — прогоняют код на копии реального состояния сети. Так интеграция с настоящими оракулами, пулами и токенами проверяется в боевых условиях, без деплоя в прод.
- Инвариант-тесты — самый мощный слой: вы формулируете свойства, а фаззер генерирует случайные последовательности вызовов, пытаясь их нарушить.
Почему инварианты важнее отдельных кейсов
Юнит-тест проверяет сценарии, которые вы придумали. Но взломщик ищет сценарий, который вы не придумали. Инвариант переворачивает задачу: вместо «проверь, что в случае X результат Y» вы говорите «что бы ни случилось, свойство P должно держаться», и инструмент сам ищет контрпример. Хорошие инварианты DeFi: «сумма пользовательских балансов не превышает активов контракта», «никто не может вывести больше, чем внёс», «общее предложение не меняется без mint/burn».
// Инвариант-тест (Foundry): фаззер дёргает функции в случайном порядке
// и после КАЖДОЙ последовательности проверяет свойство
function invariant_solvency() public {
// активов контракта должно хватать на все обязательства
assertGe(token.balanceOf(address(vault)), vault.totalLiabilities());
}Как работает под капотом: инвариант-фаззинг
Движок инвариант-тестов случайно выбирает, какие функции контракта вызвать, с какими аргументами и в каком порядке, прогоняя тысячи таких «прогонов». После каждой последовательности он проверяет все объявленные инварианты. Если хоть один нарушен — выводит точную цепочку вызовов, которая сломала систему. Это близко к тому, как рассуждает атакующий, но на вашей стороне.
Инвариант-прогон:
случайно: deposit(7) -> borrow(3) -> withdraw(2) -> ...
после каждого шага: проверить invariant_solvency()
нарушено? -> показать ломающую последовательностьПокрытие крайних случаев
Отдельно тестируйте «края»: пустой пул и первый депозит, нулевые суммы, максимальные значения, экстремальные цены оракула, повторные и параллельные вызовы. Именно на краях живёт большинство экономических багов (инфляция долей, деление на малое, округление).
Частые ошибки
- Только happy-path юнит-тесты. Покрывают задуманное, не задуманное взломщиком.
- Не формулировать инварианты. Теряете самый сильный слой защиты.
- Не тестировать на форке. Интеграционные баги всплывут уже в проде.
Итоги
- Юнит → форк → инвариант: каждый слой ловит свой класс ошибок.
- Инвариант фиксирует «всегда истинное» свойство; фаззер ищет контрпример.
- Хорошие инварианты DeFi — про платёжеспособность и сохранение средств.
- Особое внимание крайним случаям: пустой пул, ноль, экстремальная цена.