SHiNE-server/shine-solana/shine/doc/programs/shine_payments.md

403 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Программа `shine_payments`
Документ описывает текущее целевое поведение программы `shine_payments`.
Текущая целевая реализация программы:
- без Anchor;
- без использования вспомогательной зависимости из `programs/common`;
- на чистом `solana_program` с ручным разбором инструкций, PDA и состояний.
Назначение программы:
- хранить общие настройки экономической модели платежей и выплат;
- принимать покупку тикетов очереди выплат;
- хранить три очереди и отдельные ticket PDA;
- выдавать лимиты менеджерам на ручное добавление тикетов;
- выполнять пошаговые выплаты из inflow-вольта.
Если код и этот документ расходятся, нужно в том же изменении синхронизировать либо код, либо документ.
## 1. Program ID
Текущий program id:
- `c4yTa4JT9EtQDCBX9LmWFK6T2gp4JGsuymFbom2EudW`
## 2. Основная модель
`shine_payments` разделяет две вещи:
1. регистрацию долга/обязательств через тикеты;
2. фактическую выплату через отдельный inflow-вольт.
Это важно:
- покупка тикета не означает немедленную выплату из вольта;
- payout выполняется отдельной инструкцией `step_payout`;
- inflow-вольт должен быть заранее или отдельно пополнен.
## 3. PDA и seed-правила
### 3.1. Single PDA
- config: `shine_payments_config`
- coef/limit: `shine_payments_coef_limit`
- queues: `shine_payments_queues`
- inflow vault: `shine_payments_inflow_vault`
### 3.2. Ticket PDA
- queue 1 seed prefix: `shine_payments_q1_ticket`
- queue 2 seed prefix: `shine_payments_q2_ticket`
- queue 3 seed prefix: `shine_payments_q3_ticket`
- второй seed: `ticket_index` в little-endian `u64`
### 3.3. Manager allowance PDA
- seed prefix: `shine_p_manager_allow`
- второй seed: `manager_wallet.as_ref()`
## 4. Состояния программы
### 4.1. `ConfigState`
Поля:
- `version: u8`
- `dao_wallet: Pubkey`
- `inflow_vault: Pubkey`
### 4.2. `CoefLimitState`
Поля:
- `version: u8`
- `coef_ppm: u64`
- `limit_usd_cents: u64`
- `call_reward_lamports: u64`
Смысл:
- `coef_ppm` — множитель payout в ppm;
- `limit_usd_cents` — лимит суммы очереди Q1;
- `call_reward_lamports` — награда вызывающему `step_payout`.
### 4.3. `QueuesState`
Поля агрегатов:
- `q1_tickets_total`
- `q1_tickets_paid`
- `q1_sum_total_usd_cents`
- `q1_sum_paid_usd_cents`
- `q2_tickets_total`
- `q2_tickets_paid`
- `q2_sum_total_usd_cents`
- `q2_sum_paid_usd_cents`
- `q3_tickets_total`
- `q3_tickets_paid`
- `q3_sum_total_usd_cents`
- `q3_sum_paid_usd_cents`
### 4.4. `TicketState`
Поля:
- `version: u8`
- `queue_id: u8`
- `index: u64`
- `is_paid: bool`
- `recipient_wallet: Pubkey`
- `payout_usd_cents: u64`
- `debt_before_usd_cents: u64`
### 4.5. `ManagerAllowanceState`
Поля:
- `version: u8`
- `manager_wallet: Pubkey`
- `q1_available_usd_cents: u64`
- `q2_available_usd_cents: u64`
- `q3_available_usd_cents: u64`
### 4.6. `VaultState`
Поля:
- `version: u8`
Это техническая метка существования inflow PDA. Лампорты лежат на самом PDA-аккаунте.
## 5. Константы и экономика
Текущие базовые значения:
- `COEF_SCALE_PPM = 1_000_000`
- `START_COEF_PPM = 5_000_000` (5.0x)
- `START_LIMIT_USD_CENTS = 1_000_000` (10_000 USD)
- `START_CALL_REWARD_LAMPORTS = 8_000_000`
- `MAX_CALL_REWARD_LAMPORTS = 10_000_000`
- `ORACLE_MAX_AGE_SECS = 120`
- цена SOL/USD берётся из Pyth.
## 6. Инструкция `init`
### Назначение
Инициализировать все базовые PDA программы.
### Создаваемые PDA
- `config_pda`
- `coef_limit_pda`
- `queues_pda`
- `inflow_vault_pda`
### Кто может вызвать
Технически любой signer, если система ещё не инициализирована.
### Что записывается
- `dao_wallet` берётся из settings/deploy config;
- `inflow_vault` указывается как PDA самой программы;
- коэффициенты и лимиты пишутся стартовыми значениями;
- очереди стартуют с нулями.
## 7. Инструкция `update_coef_limit`
### Назначение
Изменить коэффициент выплат, лимит очереди Q1 и награду за шаг выплат.
### Авторизация
Только `dao_wallet` из `ConfigState`.
### Проверки
- signer = `config.dao_wallet`
- `coef_ppm > 0`
- `limit_usd_cents > 0`
- `call_reward_lamports <= MAX_CALL_REWARD_LAMPORTS`
## 8. Инструкция `grant_manager_limits`
### Назначение
Выдать менеджеру квоты на добавление тикетов вручную.
### Авторизация
Только DAO.
### Поведение
- создаёт `manager_allowance_pda`, если её ещё нет;
- увеличивает доступные лимиты Q1/Q2/Q3 для указанного менеджера;
- не добавляет тикеты сама.
## 9. Инструкции покупки тикета
Есть три входных варианта:
- `buy_ticket`
- `buy_ticket_usd`
- `buy_ticket_sol`
Все они в итоге сводятся к одному действию:
- рассчитывается `purchase_usd_cents`;
- создаётся ticket в очереди `Q1`;
- обновляются агрегаты очереди `Q1`.
### 9.1. Общие правила
- ticket создаётся только в `Q1`;
- очередь `Q1` временно блокируется, если достигнут её суммарный лимит;
- `payout_usd_cents = purchase_usd_cents * coef_ppm / COEF_SCALE_PPM`;
- `recipient_wallet` записывается в ticket.
### 9.2. Важная деталь денежного потока
В текущей реализации при покупке тикета лампорты переводятся:
- не во inflow vault;
- а напрямую в `dao_wallet`.
То есть:
- ticket фиксирует долг на будущую выплату;
- а inflow vault — отдельный источник средств для исполнения payouts.
Эта деталь обязательно должна быть осознана при переписи: либо её сохраняют как сознательную модель, либо отдельно меняют и тогда обновляют этот документ.
## 10. Инструкция `manager_add_ticket`
### Назначение
Дать менеджеру возможность вручную создать ticket в `Q1`, `Q2` или `Q3`.
### Авторизация
Signer должен совпадать с `manager_wallet` в `manager_allowance_pda`.
### Проверки
- `queue_id` только `1`, `2` или `3`;
- `payout_usd_cents > 0`;
- доступный allowance по очереди не меньше суммы тикета;
- ticket PDA ещё не существует.
### Эффект
- создаётся ticket;
- allowance уменьшается;
- агрегаты соответствующей очереди увеличиваются.
## 11. Инструкция `step_payout`
### Назначение
Сделать один шаг исполнения очереди выплат.
### Выбор очереди
- если в `Q1` есть pending ticket, сначала обслуживается `Q1`;
- если `Q1` пустая, обслуживается `Q2`;
- если `Q1` и `Q2` пустые, обслуживается `Q3`.
### Что считается pending
```text
pending = tickets_total - tickets_paid
```
### Как выбирается ticket
Берётся следующий индекс:
```text
next_index = tickets_paid + 1
```
### Проверки
- `config_pda`, `coef_limit_pda`, `queues_pda`, `inflow_vault_pda` валидны;
- `dao_wallet` совпадает с `config.dao_wallet`;
- `next_ticket_pda` совпадает с PDA следующего тикета;
- тикет ещё не оплачен;
- `ticket_recipient_wallet` совпадает с `ticket.recipient_wallet`;
- в inflow vault достаточно средств на весь шаг.
### Сколько переводится
Для target queue:
- получателю тикета переводится `ticket_lamports`;
- DAO переводится `dao_lamports`;
- вызвавшему шаг переводится `call_reward_lamports`.
`dao_multiplier`:
- для `Q1` = `1`
- для `Q2` = `2`
- для `Q3` = `3`
То есть DAO получает:
- `1x payout_usd` для Q1;
- `2x payout_usd` для Q2;
- `3x payout_usd` для Q3.
### Источник денег
Все три перевода идут из inflow vault PDA.
### Если pending нет
Если все три очереди пусты:
- весь доступный остаток inflow vault переводится в DAO wallet.
## 12. Инструкция `change_ticket_recipient`
### Назначение
Позволить текущему получателю тикета поменять адрес получения, пока ticket ещё не исполнен.
### Авторизация
Только текущий `ticket.recipient_wallet`.
### Ограничения
Нельзя менять получателя у следующего тикета на выплату в активной очереди.
Логика:
- если в `Q1` есть pending — следующий ticket определяется в `Q1`;
- иначе если в `Q2` есть pending — следующий ticket берётся в `Q2`;
- иначе следующий ticket берётся в `Q3`;
- если текущий ticket и есть этот ближайший ticket, смена recipient запрещена.
## 13. Pyth oracle и конвертация
Программа использует Pyth SOL/USD.
Проверки oracle:
- передан именно тот oracle account, что указан в settings;
- owner oracle-аккаунта совпадает с Pyth Solana Receiver program;
- feed id совпадает с ожидаемым `PYTH_SOL_USD_FEED_ID`;
- verification level должен быть `Full`;
- цена не старше `ORACLE_MAX_AGE_SECS`;
- доверительный интервал (`conf`) не должен быть шире `ORACLE_MAX_CONFIDENCE_PPM`;
- цена положительная и корректно переводима в ratio.
Реализация чтения:
- для декодирования price update используется официальный open-source `pyth-solana-receiver-sdk`;
- ручной парсинг по фиксированным offset-ам не используется.
Внутренние преобразования:
- `lamports -> usd_cents` делаются с округлением вниз;
- `usd_cents -> lamports` делаются с округлением вверх.
Это важно, чтобы не недоплачивать обязательства при payout.
## 14. Ошибки и классы отказа
Программа должна различать как минимум:
- неверный inflow vault;
- неверный DAO wallet;
- неавторизованный DAO;
- неавторизованный manager;
- неверный manager allowance PDA;
- неверный queue id;
- тикет уже выплачен;
- неверный recipient;
- нельзя сменить recipient ближайшего тикета;
- недостаточно средств inflow vault для step payout;
- queue temporarily paused из-за лимита;
- oracle account/feed/price invalid;
- slippage exceeded.
## 15. Что должно сохраниться при переписи без Anchor
Обязательно сохранить:
- те же PDA seed-правила;
- те же состояния и поля;
- ту же модель очередей Q1/Q2/Q3;
- ту же приоритетность `Q1 -> Q2 -> Q3` в `step_payout`;
- ту же логику allowance менеджера;
- те же oracle-ограничения и округления;
- ту же текущую модель денежных потоков, если она не меняется отдельным решением.
Если при переписи вы решите поменять экономическую модель, сначала нужно обновить этот документ.