solana: закрыть griefing создания PDA и заморозку выплат, добавить аудит №2

shine_payments + shine_users:
- create_pda_account переведён на «создание поверх предзаполненного»
  (allocate+assign+добор ренты), чтобы подсев лампортов на детерминированный
  адрес PDA (тикет/логин) не блокировал создание — закрыт LOW из аудита №1;
  в shine_payments is_uninitialized_account перестала зависеть от баланса.

shine_payments (HIGH из аудита №2):
- запрещён recipient == inflow_vault в buy_ticket*, manager_add_ticket и
  change_ticket_recipient; добавлена защита по умолчанию в transfer_from_vault
  (require vault.key != recipient.key). Это убирает алиасинг аккаунта в
  step_payout, который навсегда замораживал очередь выплат и средства вольта.

Документация и учёт:
- doc/programs/shine_payments.md §3.4, §10.1; doc/programs/shine_users.md §3.3;
- Dev_Docs/audit: добавлен аудит №2, обе закрытые находки помечены ИСПРАВЛЕНО;
- Dev_Docs/Pending_Features: две записи на ручную e2e-проверку на devnet;
- VERSION.properties: client 1.2.161, server 1.2.150.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
AidarKC
2026-06-11 04:10:31 +04:00
co-authored by Claude Fable 5
parent 01d9553db4
commit cf6a2830c8
9 changed files with 389 additions and 16 deletions
@@ -1007,11 +1007,35 @@ fn transfer_lamports<'a>(payer: &AccountInfo<'a>, recipient: &AccountInfo<'a>, s
invoke(&ix, &[payer.clone(), recipient.clone(), system_program_ai.clone()])
}
// Создание PDA, устойчивое к «минированию» детерминированного адреса.
// Адрес будущей записи логина выводится из самого логина, поэтому злоумышленник
// может заранее вычислить адрес и перевести на него немного лампортов обычным
// system-переводом. Тогда обычный system_instruction::create_account упал бы
// («account already in use») и заблокировал бы регистрацию этого логина навсегда.
// Чтобы это исключить, при уже существующих на адресе лампортах создаём аккаунт
// «поверх предзаполненного»: доводим ренту переводом, затем allocate + assign
// под подписью PDA. Подсев чужих лампортов больше ничего не ломает.
fn create_pda_account<'a>(payer: &AccountInfo<'a>, pda: &AccountInfo<'a>, system_program_ai: &AccountInfo<'a>, owner: &Pubkey, seeds: &[&[u8]], space: usize) -> ProgramResult {
let rent = Rent::get()?;
let lamports = rent.minimum_balance(space);
let ix = system_instruction::create_account(payer.key, pda.key, lamports, space as u64, owner);
invoke_signed(&ix, &[payer.clone(), pda.clone(), system_program_ai.clone()], &[seeds])
let required_lamports = rent.minimum_balance(space);
let current_lamports = pda.lamports();
if current_lamports == 0 {
// Быстрый путь: адрес пуст — обычное создание аккаунта одной инструкцией.
let ix = system_instruction::create_account(payer.key, pda.key, required_lamports, space as u64, owner);
return invoke_signed(&ix, &[payer.clone(), pda.clone(), system_program_ai.clone()], &[seeds]);
}
// На адресе уже лежат лампорты (вероятно, «подсев» атакующим). Доводим баланс
// до рент-экземпта, выделяем место и назначаем владельцем нашу программу.
let top_up = required_lamports.saturating_sub(current_lamports);
transfer_lamports(payer, pda, system_program_ai, top_up)?;
let allocate_ix = system_instruction::allocate(pda.key, space as u64);
invoke_signed(&allocate_ix, &[pda.clone(), system_program_ai.clone()], &[seeds])?;
let assign_ix = system_instruction::assign(pda.key, owner);
invoke_signed(&assign_ix, &[pda.clone(), system_program_ai.clone()], &[seeds])
}
fn ensure_pda_size_and_rent<'a>(pda: &AccountInfo<'a>, payer: &AccountInfo<'a>, system_program_ai: &AccountInfo<'a>, required_len: usize) -> ProgramResult {