Юніт-тестування смарт-контрактів: Foundry, Solidity, повне покриття

Ми часто бачимо проєкти, які йдуть на зовнішній аудит з 20% покриттям тестами — і отримують звіт на 40 сторінок, де половина помилок могла бути виловлена звичайним тест-сьютом. Наша практика: пишемо юніт-тести паралельно з розробкою контракту на [Solidity](https://en.wikipedia.org/wiki/Solidity), ви

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

Часті запитання

Останні роботи

  • 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

Ми часто бачимо проєкти, які йдуть на зовнішній аудит з 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 та інфраструктури. Гарантуємо, що ваш контракт отримає покриття, достатнє для проходження аудиту. Зв'яжіться з нами для безкоштовної оцінки вашого проєкту — ми підберемо оптимальний обсяг тестів.