Аудит смарт-контрактів Solana — це системний аналіз програм на Rust (зазвичай написаних за допомогою фреймворку Anchor) з метою виявлення логічних вразливостей, помилок роботи з обліковими записами та проблем безпеки, які можуть призвести до втрати коштів або порушення інваріантів протоколу. Нижче — покрокова методологія, яка охоплює від підготовки середовища до фінального звіту.
Методологія аудиту: ручний + автоматизований
Ефективний аудит поєднує автоматизовані інструменти для швидкого виявлення відомих патернів помилок із ручним аналізом, який знаходить логічні вразливості, недоступні статичним аналізаторам.
Підготовка середовища
Перед початком аудиту переконайтеся, що:
- Зібрано повний перелік програм (crate) у репозиторії, включно з залежностями, що належать проєкту.
- Наявна документація архітектури: опис ключових облікових записів (PDA — Program Derived Address), їхніх seed-ів, призначення та життєвого циклу.
- Налаштовано локний test-validator для відтворення станів та перевірки гіпотез.
- Визначено межі аудиту: які програми підлягають перевірці, які зовнішні програми (наприклад, Token Program, System Program) вважаються довіреними.
Автоматизований етап
Автоматизований аналіз виконується першим і слугує для формування початкового списку підозрілих місць:
- Статичний аналіз Rust-коду. Використовуйте clippy з увімкненими lint-правилами для виявлення небезпечних патернів: unwrap() на зовнішніх даних, приведення типів без перевірки, небезпечні блоки
unsafe. - Аналіз залежностей. Перевірте
Cargo.lockна наявність застарілих або відомо вразливих crate-ів. Зверніть увагу на версії Anchor та Solana SDK — перевірте актуальність у офіційних джерелах перед початком аудиту. - Фаззинг. Інструменти на кшталт
cargo-fuzzдозволяють подавати випадкові дані на вхід інструкцій і виявляти паніки (panics) та некоректні стани. Фаззинг особливо корисний для функцій десеріалізації та обробки зовнішніх параметрів.
Очікуваний результат автоматизованого етапу — список локацій у коді з потенційними проблемами, які потребують ручного підтвердження або спростування.
Ручний етап
Ручний аналіз є основою аудиту і включає:
- Читання коду інструкцій від початку до кінця. Для кожної інструкції відстежується потік даних: які облікові записи передаються, як вони десеріалізуються, які перевірки виконуються, які зміни записуються назад.
- Аналіз моделі облікових записів. Перевірка того, чи кожен PDA має однозначну деривацію, чи немає колізій seed-ів між різними типами облікових записів, чи коректно обробляється випадок, коли обліковий запис вже існує або навпаки — відсутній.
- Аналіз міжпрограмних викликів (CPI — Cross-Program Invocation). Кожен CPI вимагає перевірки: чи передаються правильні signer-привілеї, чи не можна підмінити цільову програму, чи коректно обробляються помилки, повернуті викликаною програмою.
- Перевірка прав доступу. Хто може ініціювати кожну інструкцію, чи достатньо перевірок авторизації, чи немає шляхів обійти їх через інші інструкції.
Перевірка коректності Anchor constraints
Anchor-констрейнти — це декларативні перевірки, які фреймворк виконує до входу в логіку інструкції. Помилка в констрейнтах означає, що небезпечний стан може досягти виконавчого коду.
Типові констрейнти та що перевіряти
| Констрейнт | Що перевіряти |
|---|---|
#[account(mut)] |
Чи дійсно цей обліковий запис потрібно мутувати? Надмірне використання mut розширює поверхню атаки та ускладнює аудит. |
seeds = [...], bump |
Чи збігаються seed-и з тими, що використовуються в клієнтській частині для деривації? Чи використовується bump коректно — з канонічною деривацією, а не зі збереженим у даних значенням без додаткової перевірки? |
payer |
Чи збігається payer з тим, хто повинен нести витрати на rent? Чи немає сценарію, де payer можна підмінити? |
space |
Чи достатній розмір для всіх полів структури з урахуванням видалення (optionality) та майбутніх змін? Недостатній space призводить до помилки запису після реалокації. |
authority, has_one |
Чи вказує на правильне поле в структурі облікового запису? Чи не можна змінити це поле через іншу інструкцію до виконання перевірки? |
close = target |
Чи коректно вказано цільовий обліковий запис для отримання lamports? Чи перевіряється, що закривається саме той обліковий запис, який очікується? |
Специфічні пастки Anchor
- Відсутність констрейнту
constraintдля критичних інваріантів. Anchor не вгадує бізнес-логіку. Якщо обліковий запис повинен бути в певному стані (наприклад,status == Active), це треба вказати явно. - Ігнорування поверненого значення
init. При використанніinitAnchor автоматично викликає System Program для створення облікового запису. Якщо в транзакції передано вже існуючий обліковий запис з тими ж seed-ами,initвидасть помилку, але це треба перевірити — чи не існує шлях обійти це через попереднє закриття облікового запису в тій самій транзакції. - Некоректна робота з
Option<Pubkey>у констрейнтах. Порожнє значення опціонального поля може несподівано проходити перевіркуhas_one, якщо не додано явну перевірку наSome.
Тестування на edge cases та атаки
Після статичного аналізу кожна знайдена гіпотеза або типова вразливість повинна бути підтверджена або спростована через тестування на локному test-validator.
Класи атак, які перевіряються обов'язково
- Підміна облікових записів (account spoofing). Передача стороннього облікового запису замість очікуваного PDA. Перевіряється спробою передати обліковий запис з коректною структурою даних, але іншою деривацією.
- Повторна обробка транзакції (replay). Хоча Solana захищена від дублікатів транзакцій на рівні кластера, перевірте, чи немає логічних шляхів повторного використання одного облікового запису-проміжного стану для подвійного виконання дії.
- Переповнення облікового запису (account data overflow). Спроба записати більше даних, ніж виділено
space. Особливо актуально для динамічних структур (Vec, String) у Rust. - CPI-атаки. Виклик вашої програми через іншу програму з підміною облікових записів у контексті CPI. Перевірте, чи ваші програми коректно працюють як CPI-клієнти і чи не довіряють вони неперевіреним цим з контексту виклику.
- MEV-вектори. Чи можна фронтранити або сендвічити транзакції протоколу? Це особливо актуально для DeFi-програм із ціновими оракулами або ліквідаціями.
- Виснаження обчислювального бюджету (compute budget exhaustion). Спроба змусити програму витратити весь обчислювальний бюджет до завершення критичної логіки, залишивши зміни в пам'яті незбереженими.
Методика тестування
Для кожного edge case:
- Сформулюйте гіпотезу: «Якщо передати обліковий запис X замість Y, інструкція Z виконається з неправильним авторизаційним контекстом».
- Напишіть тест, який відтворює цей сценарій. Тест повинен бути мінімальним — лише необхідні інструкції для створення початкового стану та виклику цільової інструкції.
- Запустіть на test-validator. Очікуваний результат — або помилка транзакції з коректним кодом помилки, або підтвердження вразливості.
- Зафіксуйте результат: гіпотеза підтверджена, спростована або потребує додаткового аналізу.
Усі тести, що підтверджують вразливість, повинні бути збережені як докази та включені до звіту з повним описом кроків відтворення.
Формат звіту та рекомендацій
Звіт про аудит — це технічний документ, який повинен дозволити розробникам відтворити кожну знайдену проблему та перевірити, що виправлення її усуває.
Структура запису про вразливість
Кожен знайдений issue оформляється за таким форматом:
- Назва. Коротке технічне описання проблеми (наприклад, «Missing signer check in
withdrawinstruction allows unauthorized fund extraction»). - Серйозність. Critical / High / Medium / Low / Informational. Серйозність визначається за комбінацією ймовірності експлуатації та масштабу наслідків, а не за суб'єктивною оцінкою.
- Локація. Файл, рядок коду, назва інструкції та конкретний фрагмент, де проявляється проблема.
- Опис. Фактична причина помилки: що саме відбувається в коді, чому це небезпечно, які інваріанти порушуються.
- Кроки відтворення. Послідовність дій на test-validator для експлуатації. Містить конкретні параметри транзакцій, облікові записи та очікуваний результат.
- Рекомендація щодо виправлення. Конкретна зміна в коді, а не загальна порада. Наприклад: «Додати
constraint = authority.key() == ctx.accounts.authority.key()до структури облікового запису» замість «Перевіряйте авторизацію». - Очікувана поведінка після виправлення. Як саме тест відтворення повинен змінити результат — від успішного виконання до помилки з конкретним кодом.
Загальна структура звіту
- Огляд. Обсяг аудиту (кількість програм, рядків коду), терміни, перелік залежностей та їхні версії.
- Архітектурні зауваження. Проблеми, які не є вразливостями, але ускладнюють безпеку: надмірна складність, неявні залежності між обліковими записами, відсутність інваріантів на рівні архітектури.
- Знайдені проблеми. Відсортовані за серйозністю, у форматі, описаному вище.
- Статистика. Кількість знайдених проблем за рівнем серйозності без оцінки «загального стану безпеки» — ця оцінка суб'єктивна і не несе інженерної цінності.
- Межі аудиту. Чого явно не перевірялося: клієнтський код, офчейн-компоненти, зовнішні програми, економічна модель.
Важливі обмеження
- Аудит не гарантує відсутність вразливостей. Він гарантує, що певний клас помилок перевірено на певний момент часу з певним обсягом коду.
- Зміни в коді після аудиту інвалідують результати для змінених фрагментів. Рекомендація — проводити повторний аудит або хоча б ручний огляд для кожного коміту, що змінює логіку інструкцій або констрейнти.
- Цей матеріал описує загальну методологію і не є заміною професійного аудиту конкретного проєкту. Для production-середовищ обов'язковий незалежний експертний перегляд.