Безпека програм на Solana — це не додаток до розробки, а фундаментальна умова виходу в production. На відміну від EVM-мереж, де основний вектор атак зосереджений навколо reentrancy та gas-маніпуляцій, Solana пропонує іншу модель: паралельне виконання, облікові записи з фіксованим розміром, PDA (Program Derived Address) та CPI (Cross-Program Invocation). Кожна з цих механік створює власний клас вразливостей. Цей матеріал систематизує практичні знання про безпеку програм: від керування upgrade authority до реагування на інциденти в production.
Безпечне керування upgrade authority
Що таке upgrade authority та які ризики
Кожна програма на Solana після розгортання отримує обліковий запис програми (Program Account) і окремий ключ — upgrade authority. Цей ключ дозволяє замінити байткод програми на новий. Якщо upgrade authority належить одному ключу без додаткових захисних механізмів, компрометація цього ключа означає повний контроль зловмисника над програмою: можливість вкрасти кошти, змінити логіку, вивести ліквідність.
Ризики не обмежуються лише зовнішніми атаками. Втрата доступу до ключа upgrade authority робить програму незмінною назавжди — це може бути як навмисним кроком (immutable program), так і операційною помилкою.
Перевірка поточного власника authority
Перед будь-якими операціями з програмою перевіряйте, хто саме володіє upgrade authority:
- Через CLI: solana program show <PROGRAM_ID> --output json — поле authority вказує поточного власника.
- Через RPC: виклик getAccountInfo для облікового запису програми з парсингом даних оновлення.
Якщо authority належить одиночному ключу, який не є multisig — це червоний прапорець для будь-якої програми, що керує значущими коштами.
План дій при компрометації authority
- Негайна ротація: якщо використовується multisig, ініціюйте переведення upgrade authority на новий multisig-акаунт. Поки старий multisig активний, зловмисник з одним ключем не зможе здійснити оновлення самостійно.
- Екстрений розгортання: підготуйте виправлену версію програми та проведіть оновлення через multisig якомога швидше.
- Аналіз впливу: перевірте історію транзакцій — чи було здійснено оновлення програми без санкціонованого рішення multisig.
- Комунікація: повідомте користувачів та інтеграторів про інцидент і вжиті заходи.
Типові помилки при роботі з authority
- Залишення upgrade authority на hot-ключі (ключ, який використовується для щоденних операцій або зберігається на машині розробника).
- Передача authority на акаунт без попередньої перевірки його типу та конфігурації.
- Відсутність документації щодо того, хто має доступ до ключів multisig і як відбувається ротація.
Multisig для Solana-програми
Архітектура multisig: Squads, Anchor Multisig
Multisig-рішення для Solana розподіляють upgrade authority між кількома підписувачами. Два основні інструменти:
- Squads (раніше Squads Protocol): поточний стандарт для управління multisig на Solana. Підтримує ієрархічні ключі, часові замки (time locks), різні пороги для різних типів транзакцій.
- Anchor Multisig: простіше рішення, яке підходить для базових сценаріїв з фіксованим порогом підписів. Менш гнучке у конфігурації.
Вибір залежить від розміру команди та складності процесу прийняття рішень. Для програм, що керують значними коштами, Squads забезпечує кращу гранулярність контролю.
Інтеграція multisig у процес розгортання
Процес розгортання з multisig відрізняється від прямого оновлення:
- Зберіть байткод програми (solana program deploy --output-dir ... з прапорцем --no-deploy або використовуйте solana program write-buffer).
- Створіть транзакцію оновлення програми, де signers включають multisig-акаунт, а не індивідуальний ключ.
- Пропустіть транзакцію через процес підписання multisig (кожен підписувач підписує окремо).
- Виконайте фінальну транзакцію після досягнення порогу.
Передумови: налаштований multisig-акаунт з upgrade authority програми, зібраний buffer-акаунт з новим байткодом. Очікуваний результат — програма оновлена без використання одиночного ключа. Ризик: неправильна конфігурація multisig може заблокувати оновлення. Спосіб перевірки — виклик solana program show після оновлення. Відкат: розгортання попередньої версії через той самий multisig-процес.
Ризики централізації в multisig
Multisig не усуває централізацію повністю — він її розподіляє. Якщо всі підписувачі належать одній організації або фізично знаходяться в одному юрисдикційному просторі, ризик одночасної компрометації зростає. Додатково, якщо поріг підписів встановлено на 1 з N — це не multisig, а багатоключовий акаунт без реального захисту.
Відновлення доступу при втраті ключів
Якщо один або кілька ключів multisig втрачено, можливості відновлення залежать від конфігурації:
- Якщо поріг підписів все ще досяжний з рештою ключів — ініціюйте зміну складу multisig (видалення втрачених ключів, додавання нових).
- Якщо поріг недосяжний — програма стає фактично immutable. Це підкреслює важливість вибору порогу: для N ключів поріг N-1 або N є надмірним і створює цей ризик.
Як проводити security review програми
Чеклист для самостійного огляду
Перед залученням зовнішніх аудиторів проведіть структурований самоогляд:
- Перевірте всі CPI-виклики: чи правильно ініціалізовані акаунти, чи передані правильні signers, чи оброблені помилки повернення.
- Перевірте всі PDA-деривації: чи seeds формуються з даних, які контролює програма, а не з користувацького вводу.
- Перевірте обмеження розміру даних у кожному обліковому записі: чи є ризик переповнення при розширенні.
- Перевірте відсутність неперевірених підписів та відсутність довільного запису в акаунти через CPI.
- Перевірте коректність Anchor-обмежень (constraint, has_one, seeds, mut).
Використання статичних аналізаторів
Статичний аналіз дозволяє виявити певні класи вразливостей без виконання коду. Для Solana-програм на Rust доступні:
- cargo audit: перевіряє залежності на відомі вразливості (CVE).
- Clippy з агресивними lints: виявляє підозрілі патерни — незмінні змінні, зайві приведения типів, потенційні panics.
- Спеціалізовані інструменти: перевірте актуальність доступних Solana-аналізаторів на момент проведення review, оскільки екосистема інструментів змінюється.
Обмеження: статичний аналіз не виявляє логічні помилки бізнес-логіки, помилки в семантиці Anchor-обмежень чи проблеми, пов'язані з порядком виконання транзакцій.
Коли залучати зовнішніх аудиторів
Зовнішній аудит обов'язковий, якщо програма керує коштами сторонніх користувачів, є частиною DeFi-протоколу або інтегрується з іншими протоколами як довірений компонент. Самоогляд та статичний аналіз доповнюють, але не замінюють зовнішній аудит. Оптимальний момент — після завершення функціональної розробки та внутрішнього тестування, але до розгортання в mainnet.
Документування результатів review
Кожен знайдений випадок фіксуйте з такими полями: опис проблеми, функція або інструкція, де вона виникає, критичність (critical / high / medium / low / informational), кроки відтворення, запропоноване виправлення, статус (fixed / accepted / won't fix з обґрунтуванням). Цей документ стає базою для аудиту та для подальшого постінцидентного аналізу.
Як реалізувати access control у Anchor-програмі
Pattern: owner-only функції
Найпростіший варіант обмеження доступу — перевірка, що підписувач є власником:
- Зберігайте owner у стані програми (наприклад, у конфігураційному акаунті).
- Додайте constraint: constraint = config.owner == authority.key() у структурі валідації акаунтів.
Обмеження: цей патерн не масштабується для багаторівневих дозволів. Якщо потрібно кілька ролей з різними правами, використовуйте PDA-базований підхід.
Ролі та дозволи через PDA
Замість збереження адреси owner як звичайного pubkey, використовуйте PDA як authority для адміністративних дій:
- Створіть PDA з фіксованими seeds (наприклад, b"authority").
- Програма сама підписує CPI-виклики від імені цього PDA через invoke_signed.
- Доступ до адміністративних інструкцій надається через перевірку, що виклик ініційовано з певної інструкції або через додатковий PDA-механізм делегування.
Це усуває ризик компрометації одиночного приватного ключа адміністратора, оскільки PDA не має відповідного приватного ключа взагалі.
Перевірка підписувача в інструкціях
У Anchor підписувача позначають mut та signer. Типова помилка — позначити акаунт як signer, але не перевірити, що це саме той signer, якого очікує програма. Завжди комбінуйте signer з явною перевіркою ідентичності через constraint або has_one.
Типові помилки реалізації access control
- Використання Signer без перевірки, хто саме підписує — будь-який валідний підписувач проходить перевірку.
- Зберігання admin-адреси в незмінному акаунті без можливості ротації — при компрометації ключа доступ неможливо змінити.
- Дозвіл на запис (mut) для акаунтів, які мають бути read-only у цій інструкції — це розширює поверхню атаки через CPI.
Як захиститися від reentrancy-подібних вразливостей
Чому Solana не має класичної reentrancy
У Solana відсутній спільний глобальний стан, який можна модифікувати під час виконання транзакції. Кожен обліковий запис має чітко визначений стан на початку транзакції, і всі зміни фіксуються атомарно в кінці. Класична reentrancy-атака (як у Solidity), де виклик повертається до функції до оновлення стану, структурно неможлива.
Перевірка стану після CPI
Незважаючи на відсутність класичної reentrancy, існує ризик, пов'язаний з модифікацією стану через CPI. Якщо ваша програма виконує CPI до іншої програми, а потім продовжує логіку, залежну від стану акаунтів — стан міг змінитися внаслідок CPI. Рішення: перевіряйте критичні поля стану після кожного CPI-виклику, якщо подальша логіка від них залежить.
Виявлення небезпечних патернів
- CPI-виклик, після якого програма читає стан акаунта, який був переданий у CPI як writable, без повторної перевірки.
- Передача signer-прав через CPI (програма підписує від імені PDA для іншої програми) — переконайтеся, що CPI-ціль не може викликати вашу програму знову з тими самими акаунтами.
- Використання invoke замість invoke_signed там, де потрібна підписка PDA — це не reentrancy, але може призвести до непередбачуваної поведінки.
Інструменти для автоматичного виявлення
Перевірте актуальні інструменти аналізу CPI-графів для Solana-програм на момент проведення review. Загальний підхід: побудова графа викликів між програмами в межах однієї транзакції та аналіз циклів та модифікацій стану.
Як безпечно працювати з oracles у Solana
Перевірка джерела даних oracle
Ніколи не припустіть, що акаунт з даними oracle належить легітимному джерелу. Завжди перевіряйте:
- Що акаунт oracle належить очікуваній програмі (перевірка owner акаунта).
- Що програма oracle є офіційною — порівняйте Program ID з документацією провайдера.
- Що структура даних відповідає очікуваній (розмір акаунта, магічні байти, версія формату).
Обробка застарілих даних oracle
Дані oracle мають часову мітку. Якщо з моменту останнього оновлення пройшло більше допустимого інтервалу — відхиліть транзакцію. Конкретне значення інтервалу залежить від вашого випадку використання: для ціноутворення в DeFi це може бути кілька секунд, для довгострокових контрактів — хвилини або години. Визначте цей поріг на етапі проєктування і реалізуйте як жорстку перевірку в коді.
Моделі fallback при відмові oracle
Якщо oracle недоступний або дані застарілі, є кілька стратегій:
- Hard fail: транзакція відхиляється. Найбезпечніший підхід для фінансових додатків.
- Fallback на інший oracle: використання другого незалежного джерела. Ризик — різні oracle можуть давати різні ціни, що створює арбітражні можливості.
- TWAP (Time-Weighted Average Price): усереднення ціни за період. Зменшує вплив короткочасних маніпуляцій, але не захищає від тривалої атаки на oracle.
Типові помилки інтеграції Switchboard та Pyth
- Використання pyth::load без перевірки publish_time — програма працює з потенційно застарілими даними.
- Передача неправильного Product Account замість Price Account у Pyth.
- Ігнорування статусу торгів (trading status) у даних Pyth — деякі продукти можуть бути позначені як неактивні.
- Для Switchboard: використання застарілих версій агента без перевірки сумісності формату даних.
Як налаштувати incident response для Solana-програми
План дій при виявленні вразливості
- Оцінка: визначте критичність — чи можна експлуатувати вразливість зараз, чи потрібні специфічні умови.
- Контейнізація: якщо експлуатація можлива — зупиніть уражені інструкції або фрізьте програму.
- Виправлення: підготуйте патч та проведіть оновлення через multisig.
- Верифікація: підтвердіть, що оновлена версія усуває вразливість, перевіривши на тестовій мережі.
- Розгортання: розгортання виправлення в mainnet.
Фріз програми як екстрений захід
Solana дозволяє встановити програму в стан freeze через upgrade authority. Заморожена програма відхиляє всі транзакції. Це ядерної зброю: використовуйте лише коли продовження роботи програми створює більшу шкоду, ніж повна зупинка. Передумова: upgrade authority має бути доступна та під контролем. Якщо upgrade authority скомпрометовано — фріз неможливий з боку легітимної команди.
Відновлення після інциденту
Після усунення вразливості та розгортання виправлення:
- Перевірте стан всіх акаунтів, які могли бути модифіковані під час експлуатації.
- За необхідності — ініціюйте міграцію стану до нових акаунтів, якщо старі скомпрометовані структурно.
- Розморозьте програму (якщо була заморожена) через розгортання нової версії.
Постінцидентний аналіз
Фіксуйте: хронологію подій, кореневу причину (недолік коду, процесу, конфігурації), часові рамки від виявлення до усунення, фінансовий вплив, зміни в процесах, які запобігатимуть повторенню. Цей документ має бути внутрішнім, але його висновки — основою для оновлення процесів розробки та review.
Як перевірити програму на перевантаження облікових записів
Ризики перевантаження: data corruption, denial of service
Облікові записи в Solana мають фіксований розмір, який встановлюється при ініціалізації. Якщо програма намагається записати більше даних, ніж дозволяє розмір акаунта — транзакція завершується помилкою. Але є тонший ризик: якщо розмір акаунта розрахований на межі, будь-яке розширення структури даних (додавання поля, збільшення масиву) може призвести до неможливості запису. Це denial of service для користувачів, чиї акаунти досягли ліміту.
Перевірка лімітів розміру акаунтів
Для кожного типу акаунта визначте:
- Мінімальний розмір при ініціалізації.
- Максимальний розмір даних, який може бути записаний за нормального використання.
- Різницю між ними — це запас, який захищає від майбутнього розширення.
Перевірте розміри через solana account <ADDRESS> або в тестах через account_info.data_len().
Патерни безпечного розширення даних
- Реініціалізація з більшим розміром: закрийте старий акаунт (збережіть дані в пам'яті програми), створіть новий з більшим розміром, відновіть дані. Ризик — втрата даних при crash між закриттям і створенням.
- Акаунт-контейнери: основний акаунт містить посилання на додаткові акаунти-сторінки (pagination). Це дозволяє масштабувати дані без реініціалізації, але ускладнює логіку читання.
- Застережне виділення: при ініціалізації виділяйте значно більше місця, ніж потрібно зараз. Витрата SOL на rent-exempt reserve вища, але це усуває ризик перевантаження.
Моніторинг використання простору акаунтів
Додайте метрики, які відстежують відсоток заповнення ключових акаунтів. Якщо акаунт наближається до 80% заповнення — це сигнал для планування міграції або розширення. Не чекайте, поки користувачі почнуть отримувати помилки запису.
Як працювати з PDA безпечно: типові помилки
Правильне формування seeds
PDA формується з набору seeds та програмного ID. Правило: seeds мають містити лише дані, які програма контролює або які є детермінованими і незмінними. Якщо seed містить значення, яке користувач може змінити (наприклад, баланс токена), PDA буде іншим після зміни — це призведе до неможливості доступу до існуючого акаунта.
Типові безпечні seeds: фіксовані рядки-префікси, pubkey користувача (не змінюється), ідентифікатори, які призначаються програмою і не залежать від зовнішнього стану.
Ризики колізій PDA
Колізія PDA виникає, коли два різні набори seeds генерують один і той самий адрес. Це практично неможливо при використанні унікальних ідентифікаторів, але можливо, якщо seeds формуються з коротких або повторюваних значень. Наприклад, використання лише індексу (u64) як єдиного seed без префікса програми створює ризик колізії з іншими PDA тієї ж програми, якщо індекси перетинаються в різних контекстах.
Рішення: завжди додавайте унікальний префікс (наприклад, b"vault", b"config") як перший seed.
Перевірка PDA на боці клієнта
Клієнтські додатки мають незалежно деривувати PDA і передавати його в транзакцію. Ніколи не довіряйте PDA, який приходить від сервера або фронтенду без перевірки. Використовуйте PublicKey.findProgramAddressSync на боці клієнта та порівнюйте результат з очікуваним значенням.
Небезпечні патерни з derive та find
- Використання find_program_address замість create_program_address у циклі для пошуку "вільного" PDA — це антипатерн, який створює непередбачувану поведінку.
- Зберігання PDA в стані програми без збереження seeds — при втраті seeds доступ до PDA неможливий.
- Використання змінних seeds для акаунтів, які мають бути стабільними (наприклад, PDA для токен-акаунта vault, де seed містить змінний параметр).
Як налаштувати freeze authority та коли це доречно
Механізм freeze authority в SPL Token
Freeze authority — це додатковий ключ, який дозволяє заморозити окремий токен-акаунт користувача. Заморожений акаунт не може відправляти або отримувати токени. Цей механізм існує на рівні SPL Token Program і не залежить від логіки вашої програми.
Сценарії законного використання
- Регуляторні вимоги: юрисдикції, які вимагають можливості блокування активів за рішенням суду або регулятора.
- Відповідь на інцидент: тимчасове заморожування підозрілих акаунтів під час розслідування.
- Керований реліз: заморожування токенів до офіційного запуску (наприклад, заморожування токенів команди з віконним розблокуванням).
Ризики для користувачів та репутації
Наявність freeze authority означає, що токен не є повністю децентралізованим. Для спільноти, яка цінує неконсенсованість, це може бути причиною відмови від використання. Крім того, компрометація freeze authority дозволяє зловмиснику паралізувати всі токен-акаунти.
Альтернативи замість freeze authority
- Відмова від freeze authority: при створенні токена не передавайте freeze authority (або передайте її на нульовий акаунт). Токен стає повністю незмінним на рівні заморожування.
- Контрактний підхід: замість заморожування на рівні SPL Token, реалізуйте логіку блокування на рівні вашої програми — користувачі добровільно взаємодіють з вашою програмою і приймають її правила.
- Time-lock: якщо freeze authority необхідна тимчасово, передайте її на time-lock контракт, який автоматично відмовиться від неї через визначений період.
Як проводити аудит смарт-контрактів Solana
Методологія аудиту: ручний + автоматизований
Ефективний аудит поєднує два підходи:
- Автоматизований: статичний аналіз (cargo audit, clippy), фаззинг (використання інструментів на кшталт honggfuzz або спеціалізованих Solana-фаззерів), перевірка форматів акаунтів.
- Ручний: читання коду інструкція за інструкцією, аналіз бізнес-логіки, перевірка коректності CPI-викликів, аналіз графу залежностей між акаунтами.
Автоматизований етап проводиться першим і формує список підозрілих місць для детального ручного аналізу.
Перевірка коректності Anchor constraints
Anchor-обмеження — це основний механізм безпеки, але вони можуть бути неправильними:
- has_one перевіряє рівність полів, але не перевіряє, що поле взагалі ініціалізоване.
- seeds з constraint можуть містити логічні помилки в умовах.
- Відсутність mut там, де акаунт має змінюватися — призведе до помилки виконання, але не до вразливості. Навпаки, наявність mut там, де не потрібно — розширює поверхню атаки.
Тестування на edge cases та атаки
Для кожної інструкції перевірте:
- Повторний виклик тієї ж інструкції з тими самими параметрами (чи не створює дублікатів стану).
- Виклик з граничними значеннями (нульові суми, максимальні значення u64, порожні масиви).
- Виклик у неочікуваному порядку (наприклад, використання акаунта до його ініціалізації, якщо програма це дозволяє).
- Передача акаунтів від інших програм як writable (чи не призводить до модифікації стану, яка порушує інваріанти).
Формат звіту та рекомендацій
Звіт про аудит має містити: область аудиту (версія коду, хеш коміту), методологію, перелік знайдених проблем з класифікацією за критичністю, для кожної проблеми — опис, кроки відтворення, вплив, рекомендацію з виправлення. Окремо — перелік спостережень, які не є вразливостями, але покращують якість коду. Увага: цей розділ описує загальну методологію; для конкретних аудиторів формат може відрізнятися.
Як реагувати на виявлену вразливість у production
Оцінка критичності та пріоритет
Класифікуйте вразливість за впливом:
- Critical: пряма втрата коштів користувачів або повна блокада протоколу. Реакція — негайна (хвилини/години).
- High: непряма втрата коштів або порушення інваріантів протоколу за певних умов. Реакція — протягом дня.
- Medium: порушення інваріантів без прямої фінансової втрати. Реакція — протягом тижня.
- Low / Informational: покращення якості коду. Реакція — у наступному циклі оновлень.
Екстрене оновлення програми
Передумови: підготовлений buffer з виправленим байткодом, доступний multisig з необхідним порогом підписів. Кроки:
- Зберіть та протестуйте виправлення на devnet (мінімум — перевірте, що інструкція, що містила вразливість, більше не експлуатується).
- Завантажте buffer у mainnet через solana program write-buffer.
- Ініціюйте транзакцію оновлення через multisig.
- Після досягнення порогу — виконайте оновлення.
- Перевірте результат через solana program show та тестову транзакцію.
Ризик: оновлення може містити нову помилку. Мінімізація: мінімальний патч (змінюйте лише уражений код, не проводіть рефакторинг одночасно з екстреним оновленням). Відкат: розгортання попередньої версії через той самий процес.
Комунікація зі спільнотою
Баланс між швидкістю інформування та мінімізацією паніки:
- Для critical-вразливостей, що активно експлуатуються — спочатку зупиніть експлуатацію (фріз або патч), потім повідомляйте.
- Для вразливостей, які ще не експлуатуються — повідомте спільноту з описом впливу та планом виправлення. Надайте розумний час для користувачів вжити заходів (наприклад, вивести ліквідність).
- Уникайте розпливчастих повідомлень — чітко вкажіть, що саме уражено, які дії потрібні від користувачів і які вже вжиті командою.
Постінцидентні зміни в процесах
Кожен інцидент у production має призвести до конкретних змін:
- Якщо вразливість була наслідком відсутності перевірки — додайте цей тип перевірки в обов'язковий чеклист review.
- Якщо час реакції був надто довгим — перегляньте процес доступу до multisig та готовність emergency-патчів.
- Якщо вразливість мала бути виявлена на етапі аудиту — перегляньте обсяг та методологію аудиту.
Без цих змін інцидент стає просто витратою, а не інвестицією у безпеку.