# Аудит безопасности Solana-программ SHiNE — выпуск 3 (12.06.2026) Тематический аудит с фокусом на **полноту проверок входных аккаунтов** (signer / owner / каноничный PDA-адрес / system-program / sysvar инструкций / аккаунт оракула) — отвечает на вопрос «точно ли хватает всех проверок входных аккаунтов». Код перечитан целиком после исправлений аудита №2 (`Solana-audit-2-by-Claude-11июня2026.md`): - `shine_login_guard` (183 строки) — stateless-классификатор логинов, аккаунтами не пользуется; - `shine_users` (1068 строк) — реестр пользователей, PDA-записи, ed25519-подписи, экономика лимитов; - `shine_payments` (1398 строк) — очереди тикетов, выплаты из вольта, оракул Pyth. Это ручная (не-Anchor `#[derive(Accounts)]`) реализация на `solana_program`, поэтому каждая проверка аккаунта выполняется явно в коде handler-а. Перебраны: подмена аккаунтов/PDA, подмена владельца, bump-seed атаки, отсутствие signer/authority, подмена system-program и sysvar, подмена аккаунта оракула, неинициализированные/ повторно инициализируемые PDA, «лишние» аккаунты. ## Итоговый вердикт **Проверок входных аккаунтов достаточно во всех трёх программах.** По каждому handler присутствуют все требуемые классы проверок; грубых дыр (подмена PDA на чужой аккаунт, отсутствие owner/signer-проверки, использование пользовательского bump, подмена аккаунта оракула) не найдено. Все Critical/HIGH из аудитов №1 и №2 закрыты и в этом проходе подтверждены в коде. Новых эксплуатируемых пробелов в валидации аккаунтов нет; есть несколько LOW/INFO-замечаний «by design». ## Статус прошлых находок (подтверждено в коде на 12.06.2026) - 🔴 Critical #1 (economy-config PDA, `shine_users`) — закрыто: `validate_users_economy_config_pda` (адрес + `owner == program_id`) вызывается и в create, и в update перед чтением. - 🔴 Critical #2 (singleton-PDA, `shine_payments`) — закрыто: `validate_singleton_state_pda` (адрес + `owner == id()`) во всех инструкциях. - 🟠 Medium (валидация Pyth) — закрыто: пин адреса `PYTH_SOL_USD_ACCOUNT`, `owner == pyth_receiver`, `PriceUpdateV2`, `feed_id`, возраст, доверительный интервал. - 🟡 Low (griefing на предсказуемых адресах) — закрыто: `create_pda_account` создаёт «поверх предзаполненного» в обеих программах. - 🔴 HIGH аудита №2 (`recipient_wallet == inflow_vault` замораживает выплаты) — закрыто: запрет `recipient == inflow_vault` в `buy_ticket_by_purchase_usd` (стр. 1026), `process_manager_add_ticket` (стр. 747), `process_change_ticket_recipient` (стр. 878) + защита по умолчанию `require!(vault.key != recipient.key)` в `transfer_from_vault` (стр. 1278). --- ## Матрица проверок входных аккаунтов ### shine_users | Инструкция | signer | owner PDA | адрес/seed PDA | system | sysvar / подпись | прочее | |---|---|---|---|---|---|---| | `init_users_economy_config` | ✓ | `owner == system` + `data_is_empty` (анти-reinit) | деривация + сверка | ✓ | — | значения из `settings`, не из ввода | | `update_users_economy_config` | ✓ + `signer == DAO_AUTHORITY` | `owner == program_id` | деривация + сверка | — | — | `lamports_per_limit_step > 0` | | `create_user_pda` | ✓ + `signer == client_key` | user_pda `owner == system` + empty; econ_config `owner == program_id` | user_pda, econ_config, inflow_vault, login_guard — все сверены | ✓ | ed25519 (record sig idx −2, last_block idx −1) | `inflow_vault` сверен с PDA `shine_payments`; login_guard сверен дважды | | `update_user_pda` | ✓ + `signer == client_key` | user_pda `owner == program_id`; econ_config `owner == program_id` | деривация + сверка | ✓ | ed25519 + `version == old+1` + `prev_hash == hash(old)` | immutable-поля сверены с прежней записью | ### shine_payments | Инструкция | signer | owner / валидация PDA | адрес PDA | system | прочее | |---|---|---|---|---|---| | `init` | ✓ payer | все 4 PDA `is_uninitialized` | деривация + сверка | ✓ | `dao_wallet` из `settings`, нет лишних аккаунтов | | `update_coef_limit` | ✓ + `signer == config.dao_wallet` | config/coef `owner == id()` | деривация + сверка | — | границы coef/limit/reward; нет лишних аккаунтов | | `grant_manager_limits` | ✓ + `signer == config.dao_wallet` | config `owner == id()`; allowance create/read | allowance из `manager_wallet` | ✓ | `state.manager_wallet == args.manager_wallet` | | `buy_ticket` / `_usd` / `_sol` | ✓ | config/coef/queues `owner == id()` | ticket деривация + сверка + `is_uninitialized` | ✓ | oracle (key+owner+возраст+confidence), `dao_wallet == config.dao_wallet`, `recipient != inflow_vault`, slippage | | `manager_add_ticket` | ✓ | allowance/queues `owner == id()` | allowance из `signer`; ticket деривация + сверка + uninit | ✓ | `allowance.manager_wallet == signer`, `queue_id ∈ {1,2,3}`, `recipient != inflow_vault` | | `step_payout` | ✓ | все singleton-PDA `owner == id()` | ticket деривация + сверка | — | `dao_wallet == config.dao_wallet`, `inflow == config.inflow_vault`, ticket `queue/index/!is_paid/recipient`, oracle | | `change_ticket_recipient` | ✓ + `signer == ticket.recipient_wallet` | queues + ticket `owner == id()` (через `read_state`) | ticket деривация из своих `queue_id/index` + сверка | — | `!is_paid`, запрет менять «следующий к выплате», `recipient != inflow_vault` | ### shine_login_guard Аккаунты не используются (`_accounts`); программа stateless, средствами не владеет. Защита со стороны вызова реализована в `shine_users`: сверяется и адрес вызываемой программы (`login_guard_program.key == SHINE_LOGIN_GUARD_PROGRAM_ID`), и `program_id` в `get_return_data`. Подмена/подделка ответа исключены. Отдельных проверок входных аккаунтов внутри программы не требуется. --- ## 🟡 LOW / INFO — наблюдения без прямой эксплуатации ### L1. Permissionless `init` в обеих программах `shine_payments::init` и `shine_users::init_users_economy_config` может вызвать кто угодно первым. Практического эксплойта нет: все значения (включая `dao_wallet` и `DAO_AUTHORITY`) берутся из констант `settings`, а не из ввода, повторная инициализация заблокирована проверками `is_uninitialized` / `data_is_empty`. Риск низкий; при желании привязать init к ожидаемому деплой-кошельку. Совпадает с моделью «первый init = деплой». ### L2. В `shine_users` нет явной проверки «лишних аккаунтов» — ✅ ИСПРАВЛЕНО (12.06.2026) `shine_payments` в каждом handler делает `require!(account_iter.next().is_none())`. В `shine_users` такой проверки не было — лишние аккаунты в конце списка просто игнорировались (читается строго нужное количество через `next_account_info`). Это безвредно (на безопасность не влияло), но для симметрии и явности добавлено. Класс: гигиена, не уязвимость. Закрыто: во все 4 инструкции `shine_users` (`init_users_economy_config`, `update_users_economy_config`, `create_user_pda`, `update_user_pda`) после чтения фиксированного набора аккаунтов добавлено `require!(it.next().is_none(), ShineUsersError::InvalidInstruction)`. Документация — `doc/programs/shine_users.md` §3.4. ### L3. Гонка за логином (first-come) в `shine_users` — known issue Адрес `user_pda` детерминирован из логина; после закрытия griefing-подсева остаётся обычное состязание за регистрацию (front-run в мемпуле). On-chain решается только commit-reveal; для текущей модели — приемлемый риск, ранее зафиксирован в аудите №2 (L2). К проверкам аккаунтов не относится. ### L4. Экономическая устойчивость вольта (дизайн, не баг) Деньги за покупку тикетов уходят на `dao_wallet`, а выплаты `step_payout` идут из `inflow_vault`, наполняемого регистрационными комиссиями `shine_users` (коэффициент по умолчанию `START_COEF_PPM = 5x`). При недостаточном притоке регистраций вольт истощается и выплаты останавливаются (без потери средств). Это свойство экономической модели «очередь/билеты», а не дефект валидации аккаунтов — отмечено для полноты (ранее L4 в аудите №2). Мониторить баланс вольта vs обязательств. --- ## ✅ Проверено и подтверждено как корректное (по входным аккаунтам) - **Подмена PDA** невозможна нигде: всюду пара «деривация `find_program_address` + сверка полного адреса». Пользовательский bump не принимается, `create_program_address` с внешним bump не используется — bump-seed атаки исключены. - **Проверка владельца** при каждом чтении PDA: `read_state` и `validate_singleton_state_pda` (`shine_payments`) требуют `owner == id()`; `validate_users_economy_config_pda` и проверка `user_pda.owner == program_id` (`shine_users`) — перед десериализацией данных. - **Создаваемые PDA**: проверка `is_uninitialized` / `owner == system && data_is_empty` исключает повторную инициализацию и перезапись чужого аккаунта. - **signer / authority**: все handler начинают с обязательного `is_signer`; привилегированные операции дополнительно сверяют ключ с авторитетом (`config.dao_wallet`, `DAO_AUTHORITY`, `allowance.manager_wallet`, `ticket.recipient_wallet`, `client_key`). - **system-program** сверяется с `system_program::ID` там, где идёт создание аккаунта/перевод; **sysvar инструкций** сверяется с `sysvar::instructions::id()` перед ed25519-интроспекцией. - **Аккаунт оракула**: пин адреса `PYTH_SOL_USD_ACCOUNT` + `owner == pyth_receiver` + `feed_id` + возраст (120 с) + доверительный интервал (10%). - **Ed25519 в `shine_users`**: относительные индексы −1/−2, `num_signatures == 1`, все три `ix_index == u16::MAX` (offset-данные внутри самой ed25519-инструкции), сверка `program_id == ed25519_program` и pubkey/signature/message по хэшу — указать на чужую инструкцию нельзя. - **Алиасинг аккаунтов**: `recipient != inflow_vault` запрещён на входе во всех точках задания получателя + `vault.key != recipient.key` в `transfer_from_vault`. - **`inflow_vault` в `shine_users`** сверяется с PDA, выведенным из `SHINE_PAYMENTS_PROGRAM_ID` и `SHINE_PAYMENTS_INFLOW_VAULT_SEED` — комиссия не может уйти на чужой адрес. - **Реентранси** отсутствует: CPI только в System Program и в stateless `shine_login_guard` (с проверкой возвращённого `program_id`); обратных вызовов в наши программы нет. --- ## Приоритет действий 1. **LOW** — ✅ выполнено 12.06.2026: добавлено `require!(it.next().is_none(), …)` во все инструкции `shine_users` для симметрии с `shine_payments` (L2). 2. **INFO** — зафиксировать в эксплуатационной документации known-issue гонки за логином (L3) и экономику вольта (L4); рассмотреть привязку `init` к ожидаемому деплой-кошельку (L1). Критичных и высоких находок по полноте проверок входных аккаунтов в этом проходе нет. Единственная LOW-правка (L2) применена в рамках этого же изменения; код `shine_users` собирается успешно (`cargo build -p shine_users`).