Розробка смарт-контрактів на Rust (Solana)
Ми розробляємо смарт-контракти на Rust для Solana під ключ — від проектування accounts model до деплою з multisig. Solana приваблює розробників субсекундними фінальностями та вартістю транзакції в $0.00025. Але за цією швидкістю стоїть принципово інша модель виконання: accounts model замість contract storage, stateless programs, PDA-деривати замість mapping. Розробник, який прийшов з EVM, перші два тижні думає, що Solana зламана — тому що звичні паттерни Solidity тут або не працюють, або призводять до вразливостей іншого класу. Ми навчилися обходити ці граблі за 5 років практики.
Чому Solana-програми ламають EVM-розробника
Згідно з документацією Solana, програми не володіють даними — вони лише авторизують операції над переданими акаунтами. У EVM контракт зберігає свій стан всередині себе. У Solana програма — це stateless executable, а дані живуть в окремих account-ах, якими програма не володіє — вона тільки авторизує операції над ними. Це означає, що кожен виклик інструкції вимагає явної передачі всіх accounts, які будуть задіяні.
Missing signer check — розробка смарт контрактів
Перша грабля: missing signer check. Програма отримує account через AccountInfo, але не перевіряє, що переданий authority дійсно підписав транзакцію. В Anchor це ловиться через атрибут #[account(signer)] або тип Signer<'info>. У нативному Rust — через явну перевірку if !authority.is_signer { return Err(...) }. Пропустити це легко, коли пишеш перші програми.
Account substitution
Друга класична вразливість — account substitution. Програма приймає token_account і authority, але не перевіряє, що token_account.owner збігається з переданим authority. Атакуючий передає свій token_account і чужий authority — програма виконується без помилок і дренує чужі токени.
PDA derivation mismatch
Третя біль — PDA derivation mismatch. Program Derived Address обчислюється з seeds + program_id. Якщо seeds не перевірені явно через find_program_address з constraint-ом в Anchor (seeds = [b"vault", user.key().as_ref()]), атакуючий може передати довільний account, який випадково збігається з PDA за адресою, але не є легітимним.
| Уразливість | Причина | Запобігання |
|---|---|---|
| Missing signer | Не перевірено підпис | Signer<'info> або #[account(signer)] |
| Account substitution | Не перевірено owner | Перевірка token_account.owner == authority |
| PDA mismatch | Seeds не валідовано | seeds = [...] constraint в Anchor |
Як Anchor змінює рівняння безпеки?
Працюємо переважно через Anchor framework (поточна версія 0.30.x). Anchor генерує discriminator для кожного account type і перевіряє його при десеріалізації — це автоматично закриває цілий клас type confusion атак, де атакуючий передає account не того типу.
Типова структура інструкції в нашій кодовій базі:
#[derive(Accounts)]
pub struct Deposit<'info> {
#[account(
mut,
seeds = [b"vault", user.key().as_ref()],
bump,
constraint = vault.authority == user.key() @ ErrorCode::Unauthorized
)]
pub vault: Account<'info, VaultState>,
#[account(
mut,
associated_token::mint = mint,
associated_token::authority = user
)]
pub user_token_account: Account<'info, TokenAccount>,
pub user: Signer<'info>,
pub mint: Account<'info, Mint>,
pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>,
}
Constraint-и в #[account(...)] — це не просто цукор. Вони компілюються в явні перевірки перед виконанням інструкції. Якщо constraint порушено — транзакція реверт-иться до того, як логіка інструкції виконалася.
Як тестувати Solana-програми без запуску валідатора?
Для більшості unit-тестів використовуємо solana-bankrun — він підіймає синтетичний runtime в пам'яті без запуску validator-а. Тест, який на localnet зайняв би 3 секунди (слот кожні 400 мс), виконується за 50 мс — у 60 разів швидше. Це критично для fuzzing-а.
Інтеграційні тести — на localnet через anchor test. Додаємо --skip-local-validator там, де тестовий сценарій вимагає реальних програм (Associated Token Program, Metaplex) — клонуємо їх стан з mainnet через --clone.
Для фаззингу SPL-програм використовуємо Trident (Ackee Blockchain's fuzzer). Він генерує випадкові послідовності інструкцій і шукає panics, unexpected account state, integer overflow. На одному з проєктів Trident за 4 години знайшов сценарій, де послідовність init → close → reinit призводила до повторної ініціалізації account з чужими даними — Anchor discriminator при цьому проходив, тому що reinit використовував той самий тип.
Оптимізація compute units
Solana обмежує кожну транзакцію в 1.4M compute units за замовчуванням (запросити можна до 1.4M через SetComputeUnitLimit). Serialization/deserialization через Borsh — дороге задоволення для великих структур.
Практика: розбиваємо великі state структури на кілька account-ів. Замість одного ProgramState з 50 полями — кілька спеціалізованих account-ів. Менше даних десеріалізується на кожен виклик — менше CU витрачається.
Другий прийом: zero_copy акаунти через #[account(zero_copy)] в Anchor. Дані читаються напряму з пам'яті без Borsh-десеріалізації. На структурах >1KB економія 30-50% CU.
| Підхід | CU на десеріалізацію 1KB | Мутабельність |
|---|---|---|
| Стандартний Borsh | ~8000 CU | Повна |
| zero_copy (bytemuck) | ~500 CU | Обмежена (repr(C)) |
Процес розробки
Аналітика та проектування (2-5 днів). Розбираємо accounts model під задачу: які PDA потрібні, які seeds, де потрібен CPI в Token Program або Associated Token Program. Проектуємо state до написання коду — переробка accounts структури на етапі тестування коштує дорого.
Розробка (3-10 днів залежно від складності). Anchor + Rust stable. Покриваємо кожну інструкцію тестами через Bankrun. Складні сценарії (PDA lifecycle, CPI chains) — на localnet.
Security review. Проганяємо через Soteria (статичний аналіз Solana-програм) і ручний review за чеклистом: missing signer, ownership checks, PDA validation, integer arithmetic (використовуємо checked_add, checked_mul всюди).
Чеклист безпеки для Solana-програм
- [ ] Всі signer перевірені
- [ ] Ownership checks на кожному account
- [ ] PDA derivation з seeds constraint
- [ ] Арифметика з checked_ops
- [ ] Відсутність reinit-уразливостей
- [ ] Upgrade authority задана через multisig
Деплой. anchor deploy з multisig upgrade authority (Squads Protocol). Upgrade authority не повинна бути EOA — якщо приватник витече, програму перепишуть.
Що входить в роботу
- Проектування accounts model і PDA-структури
- Написання програми на Rust з Anchor
- Написання unit-тестів (Bankrun) та інтеграційних тестів (localnet)
- Security review (статичний аналіз + ручний аудит)
- Деплой з multisig upgrade authority
- Документація інструкцій та accounts
Наша команда має 5+ років досвіду в блокчейн-розробці та більше 10 реалізованих проєктів на Solana. Якщо ви розробляєте DeFi-протокол або NFT-маркетплейс на Solana — пишіть, допоможемо уникнути типових помилок. Зв'яжіться з нами, щоб оцінити ваш проєкт і отримати план розробки.







