379 lines
12 KiB
Markdown
379 lines
12 KiB
Markdown
# Программа `shine_payments`
|
||
|
||
Документ описывает текущее целевое поведение программы `shine_payments`.
|
||
|
||
Назначение программы:
|
||
|
||
- хранить общие настройки экономической модели платежей и выплат;
|
||
- принимать покупку тикетов очереди выплат;
|
||
- хранить очереди и отдельные ticket PDA;
|
||
- выдавать лимиты менеджерам на ручное добавление тикетов;
|
||
- выполнять пошаговые выплаты из inflow-вольта.
|
||
|
||
Если код и этот документ расходятся, нужно в том же изменении синхронизировать либо код, либо документ.
|
||
|
||
## 1. Program ID
|
||
|
||
Текущий program id:
|
||
|
||
- `m48pWRGWrMj3TEHjuU4zsp5Gju4e7ZaPovk8RcVt7kR`
|
||
|
||
## 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`
|
||
- второй 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`
|
||
|
||
### 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`
|
||
|
||
### 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 для указанного менеджера;
|
||
- не добавляет тикеты сама.
|
||
|
||
## 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`.
|
||
|
||
### Авторизация
|
||
|
||
Signer должен совпадать с `manager_wallet` в `manager_allowance_pda`.
|
||
|
||
### Проверки
|
||
|
||
- `queue_id` только `1` или `2`;
|
||
- `payout_usd_cents > 0`;
|
||
- доступный allowance по очереди не меньше суммы тикета;
|
||
- ticket PDA ещё не существует.
|
||
|
||
### Эффект
|
||
|
||
- создаётся ticket;
|
||
- allowance уменьшается;
|
||
- агрегаты соответствующей очереди увеличиваются.
|
||
|
||
## 11. Инструкция `step_payout`
|
||
|
||
### Назначение
|
||
|
||
Сделать один шаг исполнения очереди выплат.
|
||
|
||
### Выбор очереди
|
||
|
||
- если в `Q1` есть pending ticket, сначала обслуживается `Q1`;
|
||
- если `Q1` пустая, обслуживается `Q2`.
|
||
|
||
### Что считается 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`
|
||
|
||
То есть DAO получает:
|
||
|
||
- `1x payout_usd` для Q1;
|
||
- `2x payout_usd` для Q2.
|
||
|
||
### Источник денег
|
||
|
||
Все три перевода идут из inflow vault PDA.
|
||
|
||
### Если pending нет
|
||
|
||
Если обе очереди пусты:
|
||
|
||
- весь доступный остаток inflow vault переводится в DAO wallet.
|
||
|
||
## 12. Инструкция `change_ticket_recipient`
|
||
|
||
### Назначение
|
||
|
||
Позволить текущему получателю тикета поменять адрес получения, пока ticket ещё не исполнен.
|
||
|
||
### Авторизация
|
||
|
||
Только текущий `ticket.recipient_wallet`.
|
||
|
||
### Ограничения
|
||
|
||
Нельзя менять получателя у следующего тикета на выплату в активной очереди.
|
||
|
||
Логика:
|
||
|
||
- если в `Q1` есть pending — следующий ticket определяется в `Q1`;
|
||
- иначе берётся следующий ticket в `Q2`;
|
||
- если текущий ticket и есть этот ближайший ticket, смена recipient запрещена.
|
||
|
||
## 13. Pyth oracle и конвертация
|
||
|
||
Программа использует Pyth SOL/USD.
|
||
|
||
Проверки oracle:
|
||
|
||
- передан именно тот oracle account, что указан в settings;
|
||
- feed id совпадает с ожидаемым;
|
||
- цена не старше `ORACLE_MAX_AGE_SECS`;
|
||
- цена положительная и корректно переводима в ratio.
|
||
|
||
Внутренние преобразования:
|
||
|
||
- `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;
|
||
- ту же приоритетность `Q1` над `Q2` в `step_payout`;
|
||
- ту же логику allowance менеджера;
|
||
- те же oracle-ограничения и округления;
|
||
- ту же текущую модель денежных потоков, если она не меняется отдельным решением.
|
||
|
||
Если при переписи вы решите поменять экономическую модель, сначала нужно обновить этот документ.
|